Crea un menú AR dinámico con productos generados desde JavaScript, categorías, filtros y modelos 3D.
Nivel intermedio
Menú AR dinámico con categorías
En el curso básico creamos un menú visual con cuatro tarjetas escritas directamente en HTML. Eso funciona para aprender, pero cuando el proyecto crece se vuelve incómodo: si el restaurante cambia precios, agrega productos o quiere filtrar por categoría, tocar el HTML una y otra vez se vuelve lento y propenso a errores.
En este nivel vamos a convertir ese proyecto en una versión más profesional. El HTML dejará un espacio vacío para las tarjetas, JavaScript tendrá una lista de productos y una función se encargará de construir el menú automáticamente. Después conectaremos esos productos con MindAR para que cada plato cargue su modelo 3D correspondiente.
Qué vas a construir
Una experiencia WebAR para restaurante con fondo visual, botón para abrir menú, modal compacto, filtros por categoría, tarjetas generadas desde JavaScript, selección de modelo GLB, eventos targetFound y targetLost, botón de WhatsApp y compilador .mind integrado.
Preparación
Crear la carpeta del proyecto intermedio
Crea una carpeta llamada menu-ar-restaurante-intermedio. Ábrela en Visual Studio Code y dentro crea tres archivos: index.html, styles.css y app.js. También copia las carpetas assets, models y targets del proyecto básico.
Desde dónde partimos
Este nivel parte del código final del curso básico. Si tienes abierto el template básico, úsalo como base y guárdalo en la nueva carpeta intermedia. Las líneas que menciono a continuación están basadas en ese archivo final básico: index.html tiene el modal entre las líneas 24 y 65, styles.css tiene los estilos del modal entre las líneas 76 y 132, y app.js empieza con selectores entre las líneas 1 y 18.
Si en tu VS Code las líneas no coinciden exactamente, no pasa nada: busca el texto indicado. En programación real las líneas se mueven cuando agregamos espacios o comentarios; por eso siempre te daré línea aproximada y el bloque exacto que debes buscar.
Por qué usamos esta estructura
index.html tendrá la estructura visible de la página y la escena de MindAR. styles.css tendrá el diseño: fondo, modal, tarjetas, filtros y panel flotante. app.js tendrá la lógica: abrir el menú, crear tarjetas, filtrar productos, cargar modelos y reaccionar al marcador.
La diferencia fuerte con el básico es que ahora las tarjetas ya no estarán escritas una por una en el HTML. El HTML solo tendrá <div id="menu-grid"></div>. Ese contenedor vacío será llenado por JavaScript.
Preparar el modal para un menú generado por JavaScript
Abre index.html. Si estás partiendo del archivo final del básico, busca el modal que comienza aproximadamente en la línea 24 con <section id="menu-modal" class="menu-modal hidden" y termina aproximadamente en la línea 65 con </section>, justo antes del botón btn-back.
Qué debes hacer exactamente
Reemplaza completo ese bloque del modal básico, desde la línea donde aparece <section id="menu-modal" hasta su cierre </section>. No reemplaces la portada home-screen, no reemplaces el botón btn-back, no reemplaces el panel ui-panel y no reemplaces la escena a-scene.
En el básico, dentro del modal estaban escritas las cuatro tarjetas manualmente entre <div class="menu-grid"> y </div>. En el intermedio quitamos esas tarjetas manuales, agregamos filter-row y dejamos <div id="menu-grid" class="menu-grid"></div> vacío para que JavaScript cree las tarjetas.
Lectura guiada del HTML dinámico
menu-modal es la ventana flotante donde estará el menú. Empieza con la clase hidden porque no queremos mostrarla hasta que el usuario toque Abrir menú. menu-backdrop es la capa oscura que queda detrás del modal; al hacer clic sobre ella, JavaScript cerrará el menú.
filter-row contiene los botones de categoría. Cada botón tiene data-filter, que es un dato personalizado del HTML. JavaScript lo leerá para saber qué productos debe mostrar. menu-grid queda vacío a propósito: ahí se insertarán las tarjetas creadas con renderMenu().
Abre styles.css. En el archivo final del básico, los estilos del modal empiezan aproximadamente en la línea 76 con .menu-modal y llegan hasta la línea 132, donde termina .modal-close-button. Justo después de esa llave de cierre y antes de .menu-grid, agrega el bloque de filtros.
Dónde va este CSS
Busca esta parte del básico: .modal-close-button { ... }. Cuando veas su llave final }, baja una línea y pega el nuevo bloque .filter-row. Debe quedar antes de .menu-grid, porque los filtros viven dentro del modal y visualmente aparecen encima de las tarjetas.
No borres .menu-grid ni .food-card. Esos estilos siguen funcionando para las tarjetas, solo que ahora las tarjetas no las escribiremos en HTML sino que las creará JavaScript.
Lectura guiada del CSS
display: flex coloca los filtros en una fila. gap separa los botones sin usar márgenes manuales. overflow-x: auto permite desplazamiento horizontal si en móvil no caben todas las categorías.
En .filter-row button usamos flex: 0 0 auto para que los botones no se aplasten. La clase .active cambia el color del filtro seleccionado; JavaScript agregará y quitará esa clase según la categoría que el usuario elija.
Abre app.js. Si partes del básico, reemplaza el bloque inicial de selectores. Ese bloque comienza en la línea 1 con document.addEventListener("DOMContentLoaded", () => { y llega aproximadamente hasta la línea 18, donde está let currentProduct = null;.
Qué cambia en los selectores
En el básico teníamos const cards = document.querySelectorAll("[data-product]"); porque las tarjetas ya existían escritas en HTML. En el intermedio esa línea ya no nos sirve, porque las tarjetas todavía no existen cuando carga la página.
Por eso reemplazamos cards por dos nuevos selectores: menuGrid, que apunta al contenedor vacío donde construiremos tarjetas, y filterButtons, que apunta a los botones de categoría. Este bloque debe quedar al inicio de app.js, antes del array products.
Lectura guiada de los selectores
DOMContentLoaded espera a que el HTML exista antes de buscar elementos. homeScreen representa la pantalla inicial. menuModal es el modal del menú. menuGrid será el contenedor donde JavaScript insertará tarjetas.
filterButtons usa querySelectorAll("[data-filter]") porque necesitamos todos los botones que tengan ese atributo. currentProduct empieza en null porque al cargar la página todavía no hay ningún plato seleccionado.
Ahora agregamos la lista de productos dentro de app.js. Si estás partiendo del básico, busca let currentProduct = null;, que está aproximadamente en la línea 18. Debajo de esa línea empieza el array products.
Qué reemplazas del básico
En el básico ya existía un array products entre las líneas 20 y 49. En el intermedio lo reemplazamos por esta nueva versión porque cada producto ahora necesita dos datos nuevos: id y category. El id ayuda a encontrar el índice real del producto, y category permite filtrar el menú.
Entonces: elimina el array products básico completo, desde const products = [ hasta ];, y pega este nuevo array en ese mismo lugar.
Lectura guiada del array
Cada producto es un objeto. id es un nombre interno estable. category se compara con los filtros. file es la ruta del modelo GLB. image es la imagen que verá el usuario en la tarjeta.
name, price y desc se mostrarán en el panel flotante cuando el marcador sea detectado. scale es muy importante en WebAR: cada modelo puede venir exportado con unidades distintas, así que guardamos una escala específica por producto.
Agrega renderMenu() debajo del array products. En el básico, después del array venía la función openMenu() y luego closeMenu(). En el intermedio puedes mantener esas funciones, pero agrega renderMenu() después de closeMenu() y antes de selectProduct().
Dónde va exactamente
Busca en app.js la función function selectProduct(index). En el básico empieza aproximadamente en la línea 63. Pega renderMenu() justo encima de selectProduct(). Así primero dejamos lista la función que crea tarjetas y después dejamos la función que responde cuando una tarjeta es seleccionada.
Este orden ayuda a leer el archivo: primero están los datos, luego las funciones de interfaz, luego la función que renderiza el menú y después la función que conecta una tarjeta con MindAR.
Lectura guiada de renderMenu()
Líneas 1 a 4: si la categoría es todos, usamos el array completo. Si es otra categoría, usamos filter() para quedarnos solo con los productos que coinciden. Líneas 6 a 15:map() transforma cada producto en un botón HTML.
findIndex() busca la posición real del producto dentro del array original. Esto importa porque un producto filtrado puede estar en otra posición dentro de visibleProducts. innerHTML inserta las tarjetas en la página. Después de eso, recién podemos buscar los botones con querySelectorAll("[data-product]") y conectarles clics.
Al final, button.classList.toggle("active", ...) marca visualmente qué filtro está seleccionado. Así el usuario entiende en qué categoría está navegando.
Ahora conectamos los botones de categoría. Este bloque debe ir después de que ya exista renderMenu(), porque los eventos de filtro llaman a esa función. En el archivo final intermedio queda después de selectProduct() y antes de los eventos de MindAR.
Qué reemplaza en el básico
En el básico existía este bloque: cards.forEach((card) => { ... }), aproximadamente entre las líneas 78 y 82. Ese bloque ya no sirve igual porque las tarjetas ahora se crean dinámicamente dentro de renderMenu().
El nuevo bloque de filtros va en la zona donde antes conectábamos eventos de tarjetas fijas. Además, renderMenu(); debe ejecutarse una vez al cargar para crear las tarjetas iniciales. Si olvidas esa línea, el modal abrirá pero el menú aparecerá vacío.
Lectura guiada de los eventos de filtro
forEach() recorre todos los botones encontrados en filterButtons. A cada uno le agregamos un click. Cuando el usuario toca un filtro, usamos button.dataset.filter para leer el valor del atributo HTML.
renderMenu(button.dataset.filter) reconstruye las tarjetas con la categoría elegida. Luego llamamos a renderMenu() una vez sin parámetros para que al abrir la página aparezcan todos los productos desde el inicio.
Ahora reemplaza la función selectProduct() del básico por esta versión. En el básico empieza aproximadamente en la línea 63 con function selectProduct(index) y termina aproximadamente en la línea 76, justo antes de cards.forEach.
Qué cambia en esta función
La función sigue teniendo la misma misión: recibir un índice, buscar el producto, cargar el GLB, actualizar los textos y arrancar MindAR. Pero ahora agregamos una protección con if (!currentProduct) return; para evitar errores si llega un índice incorrecto.
También recuerda que esta función será llamada desde las tarjetas creadas por renderMenu(). Por eso ya no depende de tarjetas escritas en HTML, sino de botones generados dinámicamente con data-product.
Lectura guiada de selectProduct()
Línea 2: guardamos el producto elegido en currentProduct. Esa variable será usada también por los eventos de MindAR y el botón de WhatsApp. Línea 3: si por alguna razón el índice no existe, detenemos la función con return.
Líneas 5 y 6: quitamos el modelo anterior y asignamos el nuevo GLB con setAttribute("gltf-model", currentProduct.file). Línea 7: ocultamos el modelo con escala cero hasta que MindAR detecte el marcador. Líneas 8 a 10: actualizamos los textos del panel flotante.
Líneas 11 y 12: cerramos el modal y escondemos la portada para dejar visible la cámara. Líneas 14 a 16: iniciamos MindAR después de una pequeña pausa para que la interfaz termine de cambiar antes de pedir cámara y comenzar tracking.
Este bloque va al final de app.js, después de haber creado renderMenu(), selectProduct() y los eventos de filtro. Si partes del básico, esta zona empieza aproximadamente en la línea 84 con target.addEventListener("targetFound" y llega hasta el final del archivo.
Qué se conserva del básico
Los eventos de MindAR casi no cambian porque la escena AR sigue usando el mismo marcador y el mismo contenedor burger-entity. Lo que cambia es el origen del producto seleccionado: antes venía de tarjetas fijas del HTML, ahora viene de tarjetas creadas por JavaScript.
Si tu código no muestra el modelo, revisa este orden: primero debe existir currentProduct, luego selectProduct() debe cargar gltf-model, después debe arrancar MindAR, y finalmente targetFound debe animar la escala del modelo.
Lectura guiada de los eventos AR
targetFound es un evento emitido por MindAR cuando reconoce la imagen objetivo. Mostramos ui-panel con la clase visible y animamos la escala del modelo desde 0 0 0 hasta currentProduct.scale. Eso crea una entrada más agradable que hacer aparecer el modelo de golpe.
targetLost ocurre cuando la cámara deja de ver el marcador. Quitamos la animación y volvemos a escala cero para que el modelo no quede flotando. El botón de WhatsApp usa encodeURIComponent() porque los mensajes pueden tener espacios, signos y acentos; esa función convierte el texto en una URL válida.
El intermedio sigue usando targets/menu.mind. En index.html, la ruta aparece dentro de la escena A-Frame, aproximadamente en la línea 70 del archivo intermedio final, en el atributo mindar-image="imageTargetSrc: targets/menu.mind; ...".
Dónde va el archivo .mind
Cuando compiles el marcador, descarga el archivo, renómbralo como menu.mind y guárdalo dentro de la carpeta targets. La ruta final debe quedar así: targets/menu.mind. Si el archivo queda en otra carpeta o con otro nombre, MindAR no podrá encontrarlo.
También puedes usar un menú impreso, un individual de mesa, un flyer turístico o un marcador de práctica. Lo importante es que la imagen tenga contraste, detalles y puntos visuales suficientes.
Cómo usar el archivo generado
Después de compilar, descarga el archivo .mind, renómbralo como menu.mind y colócalo dentro de la carpeta targets. Esa ruta debe coincidir con el atributo imageTargetSrc: targets/menu.mind que aparece en la escena de A-Frame.
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ódigo final
Proyecto completo intermedio
Al final, el proyecto queda dividido en tres archivos. El HTML define la estructura, el CSS define el diseño y JavaScript genera el menú dinámico, filtra categorías y conecta la selección con MindAR.