Construye desde cero un menú de restaurante en realidad aumentada con MindAR, A-Frame y modelos GLB.
Nivel básico
Proyecto: menú AR con página de restaurante y modal
El proyecto ahora se parece más a una experiencia real: primero hay una página con foto de fondo del restaurante, luego un botón abre un modal llamado Menú, allí aparecen cuatro tarjetas pequeñas y desde cada tarjeta se inicia la experiencia de realidad aumentada.
Visual Studio Code será el editor donde crearemos los archivos. Allí escribiremos HTML para la estructura, CSS para el diseño y JavaScript para el comportamiento del menú AR.
Entra a https://code.visualstudio.com/.
Descarga la versión para tu sistema operativo.
Instala con las opciones por defecto.
Abre VS Code.
Video: instalación de Visual Studio CodeRevisa este video si nunca has instalado VS Code.
Servidor local
Instalar y usar Live Server
WebAR debe probarse desde una URL local, no con doble clic. Live Server crea una dirección como http://127.0.0.1:5500, que se parece más a un sitio real.
Abre extensiones en VS Code.
Busca Live Server.
Instala la extensión de Ritwick Dey.
Luego podrás abrir index.html con Open with Live Server.
Video: instalación de Live ServerEste paso evita errores de rutas, modelos y cámara.
Video: probar con Live ServerAsí se abre el proyecto desde VS Code.
Proyecto
Crear carpetas y copiar recursos
Crea una carpeta llamada menu-ar-restaurante. Dentro crea los archivos index.html, styles.css y app.js. También crea assets, models y targets. Copia Restaurante.png y las imágenes de productos en assets.
Dónde va cada archivo
assets/Restaurante.png: fondo de la página.
assets/1.png a assets/4.png: imágenes de tarjetas.
models/1.glb a models/4.glb: modelos 3D.
targets/menu.mind: marcador compilado.
HTML
Crear primero un HTML mínimo
Antes de crear el menú AR, vamos a crear una página HTML sencilla. Esto confirma que el archivo existe, que el navegador lo abre y que entiendes la estructura básica.
Dónde va este código: abre menu-ar-restaurante/index.html y pega este archivo completo desde la línea 1.
Línea 2: abre el documento y define idioma español.
Líneas 3 a 7: contienen configuración interna.
Línea 5: adapta la página a móviles.
Líneas 8 a 10: muestran un título sencillo.
Lectura guiada del HTML minimo
Este primer archivo no tiene AR, menu ni estilos. Esa es la idea: antes de construir una experiencia WebAR necesitamos comprobar que el estudiante entiende la estructura de una pagina web.
<!doctype html>: no es una etiqueta visual. Es una instruccion para el navegador. Le dice: interpreta este documento con las reglas modernas de HTML.
<html lang="es">: abre todo el documento. El atributo lang="es" ayuda al navegador, lectores de pantalla y buscadores a entender que el contenido esta en espanol.
<head>: contiene informacion que configura la pagina, pero normalmente no se ve en pantalla.
meta charset="utf-8": evita problemas con tildes, eñes y simbolos.
viewport: es clave para movil. Sin esta linea, el navegador podria mostrar la pagina como si fuera de escritorio y luego reducirla, haciendo que botones y textos queden mal.
user-scalable=no: evita zoom manual. En WebAR esto ayuda porque el zoom de navegador puede afectar la sensacion de camara y UI.
<body>: contiene lo visible. Por ahora solo hay un titulo, pero luego aqui agregaremos portada, modal, panel y escena AR.
Como se relaciona con lo que viene: este archivo sera reemplazado de forma acumulativa. No estamos creando archivos separados por cada paso; estamos haciendo crecer el mismo index.html.
HTML
Crear página con fondo y botón Abrir menú
Ahora la página deja de ser un título suelto y se convierte en una portada de restaurante. El menú todavía no aparece; solo agregamos el botón que lo abrirá después.
Dónde va este código: reemplaza todo index.html por esta versión acumulada. Las líneas nuevas son 7 a 10 y 13 a 20.
index.html con portadaindex.html
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Menú AR Restaurante</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<section id="home-screen" class="home-screen">
<div class="home-content">
<span class="subtitle">Bienvenido a</span>
<h1>Burger <span>AR</span></h1>
<p>Explora platos en 3D antes de pedir. Abre el menú, elige una hamburguesa y escanea el marcador.</p>
<button id="btn-open-menu" class="primary-menu-button" type="button">Abrir menú</button>
</div>
</section>
</body>
</html>
Explicación de la portada
En esta parte todavía no estamos creando realidad aumentada. Primero estamos creando la página normal que verá el cliente cuando entra al enlace del restaurante. Esta pantalla sirve como bienvenida y como punto de entrada hacia el menú.
section id="home-screen": crea una sección completa para la portada. Le damos un id porque JavaScript necesitará ocultarla cuando empiece la experiencia AR.
class="home-screen": conecta esta sección con el CSS. El CSS será quien ponga la imagen de fondo, la capa oscura y la distribución del contenido.
div class="home-content": agrupa el texto y el botón. Esto permite mover todo como un solo bloque y controlar su ancho en computador y móvil.
btn-open-menu: es el botón principal de la página. Más adelante JavaScript lo buscará con document.querySelector("#btn-open-menu") y le agregará un evento click para abrir el modal.
Relación con el resto del proyecto: esta portada no es la experiencia AR todavía. Es la capa inicial. Cuando el usuario elija un producto, JavaScript agregará la clase hidden a home-screen para dejar visible la cámara.
Lectura guiada de la portada
La portada cumple una funcion de experiencia de usuario: no queremos pedir la camara apenas abre la pagina. Primero damos contexto, mostramos marca y ofrecemos un boton claro para abrir el menu.
link rel="preconnect": prepara la conexion con Google Fonts. No dibuja nada por si solo, pero ayuda a que la fuente cargue mas rapido.
link href="...Poppins...": trae la fuente que usara el diseño. Despues CSS dira font-family: "Poppins".
link rel="stylesheet" href="styles.css": conecta el HTML con el CSS. Si el archivo se llama distinto o esta en otra carpeta, no se aplicara ningun estilo.
section: representa una seccion grande de contenido. La usamos para la pantalla inicial completa.
id="home-screen": es un identificador unico. JavaScript lo usara para ocultar esta portada cuando inicie la camara.
class="home-screen": es un nombre reutilizable para CSS. El CSS usara esta clase para poner la capa encima del fondo.
button id="btn-open-menu": este boton no hace nada todavia. Lo importante es que ya queda nombrado para que JavaScript lo active mas adelante.
Idea importante:id suele usarse para que JavaScript encuentre un elemento especifico. class suele usarse para darle estilo con CSS o para representar estados visuales.
HTML
Agregar el modal llamado Menú
Las cuatro tarjetas ya no ocupan toda la página. Ahora viven dentro de un modal. Esto se ve mejor en computador y en móvil porque el fondo del restaurante sigue dando contexto y el menú aparece como una ventana controlada.
Dónde va este código: reemplaza index.html por esta versión acumulada. Las líneas nuevas son 23 a 65, debajo de home-screen.
El modal es una ventana que aparece encima de la página principal. Lo usamos para que las tarjetas no ocupen toda la pantalla. Así la página conserva el ambiente del restaurante y el usuario abre el menú solo cuando lo necesita.
section id="menu-modal": contiene todo el menú. El id permite abrirlo y cerrarlo desde JavaScript.
class="menu-modal hidden": combina dos clases. menu-modal le da estilo de ventana flotante. hidden lo mantiene oculto al cargar la página.
role="dialog" y aria-modal="true": ayudan a describir el comportamiento para navegadores y tecnologías de accesibilidad.
menu-backdrop: es la capa oscura que está detrás de la caja del menú. También sirve para cerrar el modal al hacer clic fuera.
menu-dialog: es la caja real del menú. Ahí vive el encabezado y la cuadrícula de productos.
button class="food-card": cada producto es un botón para que se pueda tocar en móvil. No usamos solo div porque el usuario necesita interactuar.
data-product="0": guarda el número del producto. JavaScript leerá ese número y buscará el producto correspondiente dentro del array products.
Relación con JavaScript: cuando el usuario toque una tarjeta, JavaScript tomará el valor de data-product, cargará el modelo GLB correspondiente y cerrará el modal.
Lectura guiada del modal y tarjetas
El modal organiza el menu sin llenar toda la pantalla. Esta decision es importante para movil: las tarjetas quedan pequenas, el usuario puede cerrar el menu y la portada sigue dando contexto visual.
menu-modal hidden: aqui hay dos clases. menu-modal define el estilo general del modal. hidden representa su estado inicial: oculto.
role="dialog": comunica que este bloque funciona como ventana emergente. No cambia el diseño, pero mejora la semantica.
aria-labelledby="menu-title": conecta el modal con el titulo que lo describe.
data-close-menu: es un atributo personalizado. No viene con comportamiento automatico; lo creamos para que JavaScript encuentre el fondo oscuro y lo use para cerrar el modal.
menu-dialog: es la caja visible del modal. El fondo oscuro ocupa toda la pantalla, pero esta caja contiene el contenido real.
food-card: convierte cada producto en una tarjeta visual. Como tambien es un button, se puede tocar con dedo, mouse o teclado.
data-product="0": guarda informacion dentro del HTML. Mas adelante JavaScript leera ese valor con card.dataset.product.
img src="assets/1.png": la ruta empieza desde donde esta index.html. Por eso la carpeta assets debe estar al lado de index.html.
alt: describe la imagen. Es importante para accesibilidad y tambien ayuda si la imagen no carga.
Conexion con JavaScript: las tarjetas no cargan modelos por si solas. Solo guardan el numero del producto. La funcion selectProduct() convertira ese numero en un modelo 3D real.
HTML
Agregar panel inferior para la experiencia AR
Cuando el usuario ya esté en cámara, necesitamos un panel pequeño que muestre nombre, precio, descripción y botón de WhatsApp.
Dónde va este código: reemplaza index.html por esta versión acumulada. Las líneas nuevas son 68 a 79.
Esta interfaz no pertenece al menú inicial. Pertenece al momento en que la cámara ya está activa. Por eso está fuera del modal. El usuario primero elige un producto en el modal, después escanea el marcador y entonces este panel aparece con la información del producto elegido.
button id="btn-back": crea un botón independiente para reiniciar la experiencia. Está fuera del modal porque debe seguir disponible cuando la cámara esté activa. En JavaScript lo conectaremos con location.reload().
aside id="ui-panel": crea el panel inferior. Usamos aside porque es información complementaria a la escena AR: nombre, precio, descripción y pedido.
id="ui-title": contiene el nombre del plato. JavaScript lo actualizará con titleText.textContent = currentProduct.name.
id="ui-price": contiene el precio. JavaScript lo actualizará con priceText.textContent = currentProduct.price.
id="ui-desc": contiene la descripción. JavaScript lo actualizará con descText.textContent = currentProduct.desc.
id="btn-order": crea el botón de conversión. No solo mostramos AR; también damos una acción comercial para pedir por WhatsApp.
Relación con MindAR: este panel empieza oculto desde CSS. Cuando MindAR dispara targetFound, JavaScript agrega la clase visible y el panel sube desde abajo.
Lectura guiada del panel flotante
Este bloque prepara una interfaz para cuando el usuario ya esta en la camara. Esta separado del modal porque cumple otra funcion: acompanar la experiencia AR con informacion del producto seleccionado.
btn-back: esta fuera del modal para que siga disponible durante la camara. Su funcion final sera reiniciar la pagina y volver al inicio.
aside: semanticamente indica contenido complementario. En este caso, complementa el modelo 3D con texto y accion comercial.
ui-panel: empieza oculto con CSS. No queremos mostrar datos de producto si aun no se ha detectado el marcador.
ui-title: nace con texto generico, pero JavaScript lo reemplazara por el nombre del producto elegido.
ui-price: separa el precio para poder darle estilo diferente desde CSS.
ui-desc: permite que la descripcion cambie sin editar el HTML cada vez.
btn-order: no envia nada todavia. Luego JavaScript escuchara su clic y construira un enlace a WhatsApp.
Conexion entre archivos: HTML crea los elementos, CSS los oculta o muestra visualmente, JavaScript actualiza textos y MindAR decide el momento en que el panel debe aparecer.
HTML
Agregar A-Frame, MindAR y app.js
A-Frame crea la escena 3D. MindAR reconoce la imagen objetivo. app.js abrirá el modal, cargará modelos y controlará eventos.
Dónde va este código: reemplaza index.html por esta versión acumulada. Las líneas nuevas son 11 a 13.
Estas tres líneas conectan nuestro HTML con las herramientas que harán funcionar la experiencia. Sin ellas, la página puede mostrar botones e imágenes, pero todavía no tendría realidad aumentada.
Línea 11, A-Frame: carga el framework que permite escribir una escena 3D usando etiquetas HTML como a-scene, a-camera y a-entity. A-Frame trabaja sobre Three.js, pero nos da una sintaxis más simple para empezar.
Línea 12, MindAR: carga el sistema de reconocimiento de imágenes. Esta librería usa la cámara para comparar lo que ve el usuario con el archivo .mind.
Línea 13, app.js: carga nuestro propio JavaScript. Allí controlaremos el modal, los productos, el modelo 3D, los eventos de MindAR y WhatsApp.
defer: hace que app.js se ejecute después de que el HTML esté listo. Esto es importante porque JavaScript buscará elementos como #btn-open-menu y #burger-entity.
Relación con el siguiente paso: después de cargar las librerías, ya podemos escribir la escena a-scene. A-Frame entenderá esas etiquetas porque la línea 11 ya cargó la librería.
Lectura guiada de scripts externos
Estas lineas son dependencias. Una dependencia es codigo creado por otros que usamos para no construir todo desde cero.
A-Frame: nos permite escribir 3D usando HTML. Sin A-Frame, etiquetas como a-scene o a-entity no significarian nada para el navegador.
MindAR: agrega tracking de imagen. Es quien compara la camara con el archivo .mind.
app.js: es nuestro archivo. A diferencia de las librerias externas, aqui escribimos la logica especifica del restaurante.
Orden de carga: primero cargamos A-Frame, luego MindAR, luego nuestro app.js. Asi, cuando nuestro codigo use la escena, las librerias ya existen.
defer: evita que app.js corra demasiado pronto. Si se ejecutara antes de crear el HTML, los selectores devolverian null.
Error comun: si una libreria no carga por internet, la escena AR no funcionara. En consola suelen aparecer errores relacionados con A-Frame, MindAR o etiquetas desconocidas.
HTML
Agregar la escena MindAR
La escena AR se coloca al final del body. Empieza con autoStart: false, porque la cámara debe arrancar solo cuando el usuario seleccione un plato dentro del modal.
Dónde va este código: reemplaza index.html por esta versión acumulada. Las líneas nuevas son 82 a 95.
La escena es la zona donde ocurre la realidad aumentada. Aunque está escrita en HTML, A-Frame la convierte internamente en una escena 3D usando WebGL y Three.js.
a-scene: abre el mundo 3D. Todo lo que esté dentro de esta etiqueta pertenece a la experiencia AR.
mindar-image: activa el sistema de tracking de imágenes de MindAR dentro de la escena.
imageTargetSrc: targets/menu.mind: indica dónde está el archivo compilado del marcador. Si esta ruta está mal, MindAR no sabrá qué imagen reconocer.
maxTrack: 1: le dice a MindAR que rastree un marcador a la vez. Para este nivel básico es suficiente.
autoStart: false: evita abrir la cámara al cargar la página. Esto mejora la experiencia porque primero dejamos que el usuario elija un producto.
filterMinCF y filterBeta: suavizan el tracking. Ayudan a que el modelo no tiemble tanto cuando el marcador se mueve.
a-camera: representa la cámara virtual de A-Frame. En WebAR queda alineada con la cámara real del dispositivo.
target-burger: es la entidad asociada al marcador. Cuando MindAR reconoce la imagen, todo lo que esté dentro de esta entidad aparece sobre el marcador.
burger-entity: empieza vacío. JavaScript le asignará el atributo gltf-model cuando el usuario seleccione una hamburguesa.
a-light: agrega iluminación para que el modelo 3D no se vea plano u oscuro.
Relación con JavaScript: el HTML prepara la escena, pero no decide qué hamburguesa se ve. Esa decisión ocurre en selectProduct(), donde JavaScript cambia el atributo gltf-model de burger-entity.
Lectura guiada de escena AR
Esta parte es el puente entre la pagina normal y la realidad aumentada. La escena queda escrita desde el inicio, pero no arranca automaticamente porque usamos autoStart: false.
a-scene: es el contenedor principal de A-Frame. Todo objeto 3D, camara o luz debe vivir dentro.
mindar-image: configura MindAR dentro de la escena. Sus parametros se escriben como texto separado por punto y coma.
imageTargetSrc: debe coincidir exactamente con la ruta del archivo .mind. Si el archivo esta en targets/menu.mind, la ruta debe decir eso mismo.
maxTrack: 1: simplifica el proyecto basico porque solo esperamos un marcador activo.
autoStart: false: nos da control. La camara no se abre hasta que el usuario toca una tarjeta.
filterMinCF y filterBeta: ayudan a suavizar el movimiento del modelo sobre el marcador.
a-camera: representa la camara virtual de A-Frame. En AR se alinea con la camara del dispositivo.
mindar-image-target="targetIndex: 0": dice que esta entidad responde al primer target dentro del archivo .mind.
burger-entity: no tiene modelo todavia. JavaScript le agregara gltf-model cuando el usuario seleccione un plato.
a-light: mejora la apariencia del GLB. Sin luz, muchos modelos se ven oscuros o planos.
Conexion con el flujo: el usuario toca una tarjeta, JavaScript carga el modelo en burger-entity, MindAR espera detectar el marcador, y entonces el modelo aparece.
Target
Compilar el marcador .mind del menú
MindAR necesita un archivo .mind para reconocer una imagen. Puedes usar un flyer del restaurante, una carta impresa, un individual de mesa o el marcador de práctica.
Compilador integrado
Crear el archivo .mind para el menú del restaurante
Sube la imagen del menú, flyer, mesa, poster o marcador de práctica. MindAR generará el archivo menu.mind que usaremos en el proyecto.
No hay imágenes seleccionadas.
Selecciona una imagen para comenzar.
Cómo se conecta el .mind con el código
El archivo .mind es el puente entre la imagen física y la escena 3D. El estudiante puede pensar en él como una versión preparada de la imagen para que MindAR la reconozca rápido desde la cámara.
Sube la imagen objetivo: puede ser una carta, un flyer, un individual de mesa o un marcador de práctica.
Compila el target: el compilador analiza puntos visuales de la imagen y crea un archivo que MindAR puede leer.
Descarga el archivo: el navegador genera un archivo con extensión .mind.
Renómbralo como menu.mind: usamos ese nombre porque el HTML tiene la ruta exacta targets/menu.mind.
Guárdalo en targets/menu.mind: si lo guardas en otra carpeta, debes cambiar también la ruta en imageTargetSrc.
Error común: si la cámara abre pero nunca aparece el modelo, revisa primero que el archivo exista, que se llame exactamente menu.mind y que estés usando Live Server.
CSS
Crear fondo de restaurante y portada
El fondo se carga desde assets/Restaurante.png. Usamos cover para que llene la pantalla en móvil y computador sin deformarse.
Dónde va este código: abre styles.css y pega este bloque desde la línea 1.
Este CSS crea la primera impresión visual del proyecto. El objetivo es que la experiencia parezca una página real de restaurante antes de abrir AR.
* { box-sizing: border-box; }: hace que el ancho de los elementos incluya borde y padding. Esto evita cálculos raros en tarjetas y modales.
html, body { min-height: 100%; }: asegura que la página pueda ocupar toda la altura de la pantalla.
overflow: hidden: evita scroll accidental cuando la cámara esté activa.
background: #0f172a url("assets/Restaurante.png") center / cover no-repeat fixed: coloca la imagen de restaurante, la centra, hace que cubra toda la pantalla y evita que se repita.
.home-screen: crea una capa encima del fondo. Esta capa contiene el texto y el botón Abrir menú.
linear-gradient: oscurece el lado donde está el texto. Sin esta capa, el texto podría perderse sobre la foto.
.home-screen.hidden: define cómo se oculta la portada cuando JavaScript inicia la cámara.
width: min(100%, 560px): hace que el texto no se vuelva demasiado ancho en computador, pero que sí use todo el espacio disponible en móvil.
clamp(): permite que el título crezca o se reduzca según el tamaño de pantalla.
Relación con JavaScript: JavaScript no cambia estilos manualmente; solo agrega o quita clases como hidden. El CSS define qué significa visualmente esa clase.
Lectura guiada del CSS de portada
El CSS no es decoracion aislada. En este proyecto tambien controla estados: portada visible, portada oculta, modal visible, modal oculto y panel AR visible.
box-sizing: border-box: hace mas facil calcular tamanos. Si una tarjeta mide 100%, el padding y borde no la hacen salirse.
-webkit-tap-highlight-color: quita el resaltado tactil que algunos moviles ponen al tocar botones.
min-height: 100%: asegura que html y body ocupen la altura completa.
overflow: hidden: impide scroll de pagina cuando el usuario esta usando la camara.
background: ... url("assets/Restaurante.png") ...: une color de respaldo e imagen. Si la imagen falla, al menos queda un fondo oscuro.
cover: recorta lo necesario para cubrir la pantalla sin deformar la foto.
home-screen con position: fixed: hace que la portada cubra toda la pantalla aunque debajo exista una escena AR.
z-index: 20: coloca la portada encima de la escena hasta que JavaScript la oculte.
home-screen.hidden: define el estado oculto. JavaScript solo agrega la clase; CSS decide el efecto visual.
Por que importa: en AR es muy facil tapar la camara con fondos. Por eso debemos entender que capas estan arriba y cuales deben ocultarse.
CSS
Diseñar el modal y tarjetas pequeñas
Las tarjetas ahora son más pequeñas y viven dentro de menu-dialog. En móviles se muestran en dos columnas y en pantallas grandes pueden verse en cuatro columnas dentro del modal.
Dónde va este código: reemplaza styles.css por esta versión acumulada. Las líneas nuevas son 74 a 171.
El modal resuelve el problema de tener tarjetas enormes ocupando toda la pantalla. En móvil debe ser compacto, fácil de tocar y con scroll si hace falta. En computador debe verse más ancho, pero no como una página completa.
.menu-modal: ocupa toda la pantalla para poder centrar el diálogo y cubrir el resto con una capa oscura.
.menu-modal.hidden { display: none; }: oculta el modal completamente hasta que JavaScript quite la clase hidden.
.menu-backdrop: crea el fondo oscuro y borroso. Además el JavaScript lo usa como zona clicable para cerrar.
width: min(100%, 620px): limita el ancho del menú. En móvil ocupa el ancho disponible; en computador no crece sin control.
max-height: min(86vh, 720px): evita que el modal se salga de pantalla. Si no cabe, aparece scroll dentro del modal.
grid-template-columns: repeat(2, minmax(0, 1fr)): muestra dos tarjetas por fila en móvil, que es suficiente para que se vean pequeñas y tocables.
aspect-ratio: 1.35: mantiene todas las imágenes con una proporción estable aunque tengan tamaños diferentes.
@media (min-width: 760px): cambia la cuadrícula a cuatro columnas en pantallas grandes.
Relación con HTML: el HTML solo define tarjetas. El CSS decide si se ven en dos o cuatro columnas según el tamaño de pantalla.
Lectura guiada del CSS del modal
El modal debe sentirse como una ventana, no como otra pagina completa. Por eso limitamos ancho, altura, columnas y tamano de tarjetas.
.menu-modal con position: fixed: hace que el modal cubra toda la pantalla sin depender del scroll.
place-items: center: centra la caja del menu horizontal y verticalmente.
.menu-modal.hidden { display: none; }: elimina el modal del flujo visual cuando esta cerrado.
backdrop-filter: blur(8px): desenfoca el fondo para que la atencion vaya al menu.
width: min(100%, 620px): evita que el menu sea demasiado ancho en escritorio y evita overflow en movil.
max-height y overflow-y: auto: permiten que el modal tenga scroll interno si la pantalla es pequena.
grid-template-columns: repeat(2, ...): en movil dos columnas hacen que las tarjetas sean pequenas pero tocables.
@media (min-width: 760px): en escritorio pasamos a cuatro columnas porque hay mas espacio.
aspect-ratio: mantiene las imagenes consistentes para que las tarjetas no salten de tamano.
Relacion con el problema que corregimos: antes las cuatro tarjetas ocupaban toda la pantalla. Ahora viven dentro de un contenedor limitado y responsive.
CSS
Diseñar panel inferior AR
El panel inferior empieza oculto y sube cuando MindAR reconoce el marcador. En escritorio el panel no ocupa todo el ancho; se vuelve una tarjeta flotante a la derecha.
Dónde va este código: reemplaza styles.css por esta versión final acumulada. Las líneas nuevas son 173 a 239.
El panel inferior funciona como una ficha del producto mientras el usuario mira el modelo 3D. Debe ser visible, pero no debe tapar toda la cámara.
position: fixed: mantiene el panel pegado a la pantalla, no al contenido de la página.
bottom: 0: lo ubica abajo en móvil, donde se espera una ficha de producto.
z-index: 12: lo coloca por encima de la cámara y la escena AR.
transform: translateY(112%): lo mueve fuera de pantalla para que empiece oculto.
.ui-panel.visible: vuelve el panel a su posición normal. JavaScript agrega esta clase cuando ocurre targetFound.
.ui-panel div: organiza nombre y precio en una fila.
.whatsapp-button: usa color verde y ancho completo para que la acción de pedido sea clara en móvil.
@media (min-width: 760px): en computador el panel se vuelve una tarjeta flotante a la derecha, no una barra gigante de lado a lado.
Relación con MindAR: el panel no aparece por sí solo. Aparece cuando MindAR reconoce el marcador y JavaScript ejecuta uiPanel.classList.add("visible").
Lectura guiada del CSS del panel AR
El panel debe aparecer solo cuando el marcador se detecta. CSS define como se ve oculto y como se ve visible; JavaScript decide cuando cambiar de estado.
position: fixed: lo mantiene pegado a la pantalla, aunque la camara y el mundo 3D se muevan.
bottom: 0: en movil se comporta como ficha inferior, un patron familiar para usuarios.
transform: translateY(112%): lo desplaza hacia abajo para esconderlo sin usar display: none.
transition: permite que el panel suba suavemente.
.ui-panel.visible: es el estado que JavaScript activa cuando MindAR encuentra el marcador.
@media: en computador cambiamos el panel a una tarjeta lateral para no tapar la vista de camara.
z-index: lo pone arriba de la escena AR, pero por debajo del modal si el modal esta abierto.
Conexion con MindAR:targetFound agrega visible; targetLost la quita. CSS hace el resto.
JavaScript
Seleccionar elementos del HTML
Empezamos app.js buscando los elementos importantes: pantalla inicial, modal, botones, panel, escena AR y tarjetas de productos.
Dónde va este código: abre app.js y pega este bloque desde la línea 1.
Antes de cambiar algo con JavaScript, primero tenemos que encontrar ese elemento en el HTML. Por eso usamos document.querySelector() y document.querySelectorAll().
document.addEventListener("DOMContentLoaded", ...): espera a que el HTML esté cargado. Sin esto, JavaScript podría buscar botones que todavía no existen.
homeScreen: guarda la portada. Más adelante le agregaremos la clase hidden para esconderla al iniciar AR.
menuModal: guarda el modal. Lo abriremos y cerraremos quitando o agregando la clase hidden.
openMenuButton, closeMenuButton y menuBackdrop: son los tres puntos de interacción del modal: abrir, cerrar con X y cerrar tocando el fondo.
uiPanel, titleText, priceText y descText: forman la ficha de producto que aparecerá en AR. JavaScript cambiará su contenido según la tarjeta seleccionada.
burgerEntity: conecta JavaScript con la entidad de A-Frame donde se cargará el modelo GLB.
scene: permite acceder al sistema de MindAR para iniciar la cámara con start().
target: permite escuchar targetFound y targetLost.
cards: selecciona todas las tarjetas del menú porque todas tienen data-product.
Relación con HTML: si cambias un id en el HTML, también debes cambiar el selector en JavaScript. Por ejemplo, si cambias id="btn-open-menu", la línea que busca #btn-open-menu dejará de encontrarlo.
Lectura guiada de selectores
Estas lineas son la preparacion de JavaScript. No cambian nada en pantalla todavia; solo guardan referencias para poder trabajar con ellas despues.
const: crea una constante. La referencia al elemento no se reasignara despues.
querySelector("#home-screen"): busca un elemento por id. El signo # significa id, igual que en CSS.
querySelector("[data-close-menu]"): busca un atributo personalizado. Esto sirve cuando no queremos depender de un id.
querySelectorAll("[data-product]"): devuelve una lista de todas las tarjetas de producto.
let currentProduct = null: crea una variable vacia. Mas adelante guardara el objeto del producto seleccionado.
null: significa que todavia no hay valor. Es util para comprobar si el usuario aun no ha elegido nada.
Error comun: si querySelector devuelve null, casi siempre significa que el id esta mal escrito o que JavaScript se ejecuto antes de cargar el HTML.
JavaScript
Crear productos del menú
El array products funciona como una pequeña base de datos local. Cada objeto representa un plato del modal.
Dónde va este código: reemplaza app.js por esta versión acumulada. Las líneas nuevas son 20 a 49.
El array products es una pequeña base de datos escrita directamente en JavaScript. En lugar de repetir lógica para cada hamburguesa, guardamos sus datos en objetos y dejamos que la función selectProduct() use esos datos.
const products = [ ... ]: crea una lista. Cada elemento de la lista representa un producto del menú.
{ ... }: cada par de llaves crea un objeto. Un objeto agrupa datos relacionados: modelo, nombre, precio, descripción y escala.
file: guarda la ruta del modelo GLB. Esta ruta se usará en burgerEntity.setAttribute("gltf-model", currentProduct.file). Si el archivo no existe en esa ruta, el modelo no carga.
name: guarda el nombre comercial del plato. Se mostrará en el panel con titleText.textContent.
price: guarda el precio visible para el usuario. Se mostrará en ui-price.
desc: guarda la descripción que ayuda a vender el producto. Se mostrará en ui-desc.
scale: define el tamaño del modelo sobre el marcador. Cada GLB puede venir con un tamaño diferente, por eso cada producto tiene su propia escala.
Relación con las tarjetas: una tarjeta tiene data-product="0". Ese número apunta al producto en la posición 0 del array. Si el usuario toca esa tarjeta, JavaScript usa products[0].
Cómo modificarlo: para cambiar una hamburguesa, reemplaza sus datos en este array y asegúrate de que la imagen en HTML y el modelo en models/ coincidan.
Lectura guiada del array products
Este array evita escribir una funcion distinta para cada hamburguesa. Cada tarjeta apunta a una posicion del array y la misma funcion sirve para todos los productos.
products[0]: representa el primer objeto del array. En este proyecto coincide con la tarjeta que tiene data-product="0".
file: se conecta con A-Frame. Su valor se usara como ruta del atributo gltf-model.
name, price y desc: se conectan con la UI flotante. Estos valores reemplazan los textos iniciales.
scale: se conecta con la animacion de entrada. El modelo empieza en 0 0 0 y termina en esta escala.
Comas: cada propiedad y cada objeto necesita separarse correctamente. Un error de coma puede romper todo el archivo JavaScript.
Rutas: si escribes models/3.glb, debe existir una carpeta models al lado de index.html y dentro debe estar 3.glb.
Como crecer el proyecto: para agregar un quinto producto, agregas otra tarjeta con data-product="4" y otro objeto en la posicion 4 del array.
JavaScript
Abrir y cerrar el modal Menú
Ahora el botón Abrir menú muestra el modal. El botón de cerrar y el fondo oscuro lo ocultan. Esto mantiene la interfaz ordenada.
Dónde va este código: reemplaza app.js por esta versión acumulada. Las líneas nuevas son 51 a 63.
El modal ya existe en el HTML desde el inicio, pero empieza oculto con la clase hidden. JavaScript no crea el modal; solo cambia sus clases para mostrarlo u ocultarlo.
function openMenu(): define una función reutilizable para abrir el menú.
menuModal.classList.remove("hidden"): quita la clase que lo ocultaba. Como el CSS dice que .menu-modal.hidden tiene display: none, al quitarla el modal vuelve a aparecer.
function closeMenu(): define la función contraria.
menuModal.classList.add("hidden"): agrega la clase que oculta el modal.
openMenuButton.addEventListener("click", openMenu): cuando el usuario toca Abrir menú, se ejecuta openMenu.
closeMenuButton.addEventListener("click", closeMenu): conecta la X del modal.
menuBackdrop.addEventListener("click", closeMenu): permite cerrar tocando fuera de la caja del menú.
Relación con CSS: este patrón es muy común: JavaScript cambia clases y CSS define cómo se ve cada estado.
Lectura guiada de funciones del modal
Aqui aparece una idea central de JavaScript: escribir una funcion una vez y ejecutarla desde varios lugares.
function openMenu(): declara una accion con nombre. No se ejecuta al declararla; queda lista.
classList.remove("hidden"): modifica las clases CSS del modal. Al quitar hidden, deja de aplicarse display: none.
function closeMenu(): crea la accion contraria.
classList.add("hidden"): vuelve a ocultar el modal usando CSS.
addEventListener("click", openMenu): conecta el clic del boton con la funcion. Observa que no escribimos openMenu() con parentesis, porque queremos pasar la funcion, no ejecutarla inmediatamente.
menuBackdrop: permite una interaccion natural: cerrar tocando fuera del menu.
Relacion con CSS: JavaScript no anima el modal directamente. Solo cambia clases; CSS decide si se ve o no.
JavaScript
Seleccionar producto y arrancar AR
Cuando el usuario toca una tarjeta del modal, cargamos su modelo GLB, actualizamos el panel, cerramos el modal, ocultamos la portada y arrancamos MindAR.
Dónde va este código: reemplaza app.js por esta versión acumulada. Las líneas nuevas son 65 a 84.
Esta función conecta el menú 2D con la escena 3D. Es una de las partes más importantes del proyecto porque toma la decisión del usuario y la convierte en contenido aumentado.
function selectProduct(index): recibe el número del producto. Ese número viene de data-product en la tarjeta.
currentProduct = products[index]: busca el objeto correspondiente dentro del array de productos.
removeAttribute("gltf-model"): limpia un modelo anterior antes de cargar otro. Esto evita que queden residuos si el usuario vuelve a elegir.
setAttribute("gltf-model", currentProduct.file): carga el archivo GLB dentro de burger-entity.
setAttribute("scale", "0 0 0"): deja el modelo invisible al principio. Luego aparecerá con animación cuando MindAR encuentre el marcador.
titleText.textContent, priceText.textContent y descText.textContent: actualizan el panel inferior con los datos del producto.
closeMenu(): cierra el modal porque ya no necesitamos ver las tarjetas.
homeScreen.classList.add("hidden"): oculta la portada para que la cámara pueda verse.
setTimeout(..., 500): espera medio segundo para que la transición visual no sea brusca.
scene.systems["mindar-image-system"].start(): inicia MindAR y solicita acceso a la cámara.
Relación con la experiencia del usuario: la cámara no se abre de inmediato. Primero el usuario entiende dónde está, abre el menú, elige un plato y luego entra a AR.
Lectura guiada de selectProduct
Esta funcion es el corazon del proyecto basico. Une tarjeta, datos, modelo 3D, panel visual y camara.
index: es el numero que viene desde data-product. Si la tarjeta tiene data-product="2", entonces index vale 2.
currentProduct = products[index]: guarda el producto activo. Desde este momento, el resto del codigo sabe cual hamburguesa se eligio.
removeAttribute("gltf-model"): limpia el modelo anterior antes de cargar el nuevo.
setAttribute("gltf-model", currentProduct.file): asigna el GLB al contenedor de A-Frame.
setAttribute("scale", "0 0 0"): lo deja invisible hasta que el marcador sea detectado.
textContent: cambia texto visible en HTML sin reemplazar toda la etiqueta.
closeMenu(): cierra el modal para que la pantalla quede limpia.
homeScreen.classList.add("hidden"): oculta la portada que esta encima de la camara.
setTimeout: deja pasar un momento para que las transiciones visuales terminen antes de iniciar la camara.
mindar-image-system.start(): inicia el sistema de camara y tracking de MindAR.
cards.forEach: recorre todas las tarjetas y les agrega un evento de clic.
Number(card.dataset.product): convierte el valor de data-product en numero para usarlo como indice del array.
MindAR trabaja con eventos. No tenemos que revisar todo el tiempo si el marcador aparece; MindAR avisa cuando lo encuentra y cuando lo pierde.
target.addEventListener("targetFound", ...): escucha el momento exacto en que la cámara reconoce el marcador.
uiPanel.classList.add("visible"): muestra el panel inferior. El CSS ya sabe cómo mostrarlo porque existe la regla .ui-panel.visible.
if (!currentProduct) return: protege el código. Si por alguna razón no hay producto seleccionado, la función se detiene.
burgerEntity.setAttribute("animation", ...): agrega una animación de A-Frame al modelo.
property: "scale": indica que la animación cambiará el tamaño del modelo.
from: "0 0 0": empieza invisible.
to: currentProduct.scale: termina en la escala definida para ese producto.
dur: 800: la animación dura 800 milisegundos.
easing: "easeOutElastic": crea un efecto de entrada con rebote.
targetLost: se ejecuta cuando MindAR deja de ver el marcador.
removeAttribute("animation"): limpia la animación para que pueda repetirse la próxima vez.
setAttribute("scale", "0 0 0"): oculta el modelo cuando se pierde el marcador.
Relación con el flujo completo:selectProduct() carga el modelo, pero targetFound decide cuándo se muestra. Así el modelo no aparece flotando antes de escanear.
Lectura guiada de eventos AR
Esta parte responde a lo que ve la camara. No depende de un clic directo del usuario, sino de si MindAR reconoce o pierde el marcador.
targetFound: ocurre cuando MindAR identifica la imagen objetivo dentro de la camara.
uiPanel.classList.add("visible"): muestra la ficha del producto. La animacion de entrada la maneja CSS.
if (!currentProduct) return: protege la experiencia. Si no hay producto, no tiene sentido animar modelo.
setAttribute("animation", { ... }): usa el componente de animacion de A-Frame.
property: "scale": indica que cambiaremos el tamano.
from: "0 0 0": arranca invisible.
to: currentProduct.scale: usa la escala del producto activo. Por eso guardamos scale en el array.
dur: 800: controla la duracion en milisegundos.
targetLost: ocurre cuando el marcador sale de camara o MindAR ya no lo reconoce bien.
removeAttribute("animation"): limpia la animacion anterior para que pueda repetirse luego.
scale = 0: oculta el modelo de nuevo cuando el marcador desaparece.
Importante: cargar el modelo y mostrar el modelo son dos momentos distintos. selectProduct() carga el GLB; targetFound lo muestra sobre el marcador.
JavaScript
Agregar WhatsApp y botón volver
La experiencia termina con una acción comercial: pedir el plato por WhatsApp. El botón Volver recarga la página y permite comenzar de nuevo.
Dónde va este código: reemplaza app.js por esta versión final acumulada. Las líneas nuevas son 106 a 115.
Una experiencia WebAR para restaurante debe tener una acción. Ver el plato en 3D es atractivo, pero el objetivo comercial puede ser pedir, reservar o escribir por WhatsApp.
orderButton.addEventListener("click", ...): escucha cuando el usuario toca el botón de pedido.
if (!currentProduct) return: evita crear un pedido si todavía no se ha seleccionado ningún producto.
const message: crea el texto que se enviará a WhatsApp usando el nombre y precio del producto actual.
encodeURIComponent(message): convierte espacios y caracteres especiales para que el mensaje funcione dentro del enlace.
window.open(..., "_blank"): abre WhatsApp en una nueva pestaña o en la app si el dispositivo la tiene instalada.
backButton.addEventListener("click", ...): escucha el botón Volver.
location.reload(): recarga la página para volver al estado inicial: portada visible, menú cerrado, cámara detenida.
Mejora futura: más adelante este botón podría enviar el producto a un carrito, guardar un lead en Firebase o medir el clic con GA4.
Lectura guiada de WhatsApp
Esta seccion convierte la experiencia visual en una accion comercial. Para restaurantes y activaciones, esto suele ser lo mas valioso: llevar al usuario desde curiosidad hasta contacto.
orderButton.addEventListener("click", ...): escucha cuando el usuario toca el boton de pedido.
if (!currentProduct) return: evita enviar un mensaje vacio si no hay producto seleccionado.
const message: construye un texto usando datos del producto actual. No es texto fijo; depende de lo que el usuario eligio.
encodeURIComponent(message): convierte espacios, simbolos y caracteres especiales para que el mensaje viaje correctamente en una URL.
https://wa.me/?text=...: es el formato de enlace que abre WhatsApp con un mensaje prellenado.
window.open(..., "_blank"): abre el enlace fuera de la pagina actual para no cortar la experiencia.
backButton: da una salida simple si el usuario quiere elegir otro plato.
location.reload(): reinicia el estado completo de la experiencia. Es una solucion sencilla para nivel basico.
Mejora futura: en un nivel mas avanzado podriamos evitar recargar la pagina y en su lugar detener MindAR, limpiar el modelo y volver a mostrar el modal.
Código final
Proyecto completo del menú AR básico
Estos son los archivos finales. El modal mantiene pequeñas las tarjetas, el fondo usa la imagen del restaurante y la cámara arranca solo después de elegir un producto.