PROMPT: Simulador Educativo Interactivo con Análisis Visual por Hotspots

PROMPT: Simulador Educativo Interactivo con Análisis Visual por Hotspots


ROL Y OBJETIVO

Eres un desarrollador frontend especializado en herramientas educativas digitales. Tu tarea es crear un archivo index.html completamente autónomo que funcione como simulador educativo interactivo para analizar imágenes mediante hotspots anotados predefinidos, con cuestionario integrado y envío de respuestas por correo.

El simulador debe funcionar abriendo el archivo directamente en un navegador (sin servidor), con las imágenes en la misma carpeta. No requiere frameworks ni dependencias externas salvo Google Fonts.


ARQUITECTURA TÉCNICA OBLIGATORIA

A. Estructura de la imagen con hotspots sin clipping

Este es el problema técnico más crítico. Los hotspots deben ser siempre visibles, incluso si la imagen tiene overflow:hidden. La solución es la siguiente jerarquía de contenedores:

.poster-stage           → position: relative  (contenedor maestro)
  ├── .poster-img-wrap  → overflow: hidden     (solo clipea la imagen)
  │     └── <img>       → la imagen del póster
  └── .hs (×N)          → position: absolute, z-index: 100
                           FUERA de .poster-img-wrap
                           Nunca será clippeado

Cálculo de posición de cada .hs: Cada hotspot almacena su posición como porcentaje (pctX, pctY) relativo al área de la imagen. Al renderizar, se convierte a píxeles absolutos dentro de .poster-stage:

el.style.left = (imgWrap.offsetLeft + hs.pctX / 100 * imgWrap.offsetWidth)  + 'px';
el.style.top  = (imgWrap.offsetTop  + hs.pctY / 100 * imgWrap.offsetHeight) + 'px';
el.style.transform = 'translate(-50%, -50%)';

Usar ResizeObserver sobre imgWrap para recalcular posiciones al cambiar el tamaño de pantalla:

const ro = new ResizeObserver(() => place());
ro.observe(imgWrap);

B. Tooltip global con position:fixed

El tooltip debe crearse una única vez en JavaScript e insertarse directamente en <body>. Nunca debe estar dentro de un contenedor con overflow:hidden o overflow:auto.

// Crear e insertar en body (una sola vez al cargar)
const gTip = document.createElement('div');
gTip.id = 'g-tip';
gTip.innerHTML = `
  <div class="gt-inner">
    <div class="gt-title" id="gt-title"></div>
    <div class="gt-desc"  id="gt-desc"></div>
    <div class="gt-arrow" id="gt-arrow"></div>
  </div>`;
document.body.appendChild(gTip);

CSS del tooltip:

#g-tip {
  position: fixed;       /* relativo al viewport, nunca clippeado */
  z-index: 9999;
  pointer-events: none;
  opacity: 0;
  transition: opacity .16s ease, transform .16s ease;
  transform: translateY(5px);
  max-width: 240px;
}
#g-tip.vis { opacity: 1; transform: translateY(0); }

Algoritmo de posicionamiento dinámico (ejecutar en CADA hover):

function showTip(hsEl) {
  // 1. Leer datos del hotspot
  gtTitle.textContent = hsEl.dataset.title || '';
  gtDesc.textContent  = hsEl.dataset.desc  || '';

  // 2. Medir el tooltip (hacerlo visible oculto para obtener dimensiones)
  gTip.style.visibility = 'hidden';
  gTip.style.display = 'block';
  const tw = gTip.offsetWidth;
  const th = gTip.offsetHeight;
  gTip.style.display = '';
  gTip.style.visibility = '';

  // 3. Obtener posición del hotspot en el viewport
  const rect = hsEl.getBoundingClientRect();
  const VW   = window.innerWidth;
  const VH   = window.innerHeight;
  const gap  = 10;
  const hsCx = rect.left + rect.width / 2;   // centro horizontal del hotspot

  // 4. Decidir: mostrar ARRIBA si hay espacio, ABAJO si no
  const showAbove = (rect.top - gap) >= th;
  let top  = showAbove ? rect.top - th - gap : rect.bottom + gap;
  let left = hsCx - tw / 2;

  // 5. Clampear al viewport (nunca salir de los bordes)
  top  = Math.max(6, Math.min(top,  VH - th - 6));
  left = Math.max(6, Math.min(left, VW - tw - 6));

  gTip.style.top  = top  + 'px';
  gTip.style.left = left + 'px';

  // 6. Posicionar la flecha apuntando exactamente al hotspot
  const arrowLeft = Math.max(10, Math.min(hsCx - left, tw - 10));
  gtArrow.style.left      = arrowLeft + 'px';
  gtArrow.style.transform = 'none';
  gtArrow.className = showAbove ? 'gt-arrow down' : 'gt-arrow up';

  gTip.classList.add('vis');
}
function hideTip() { gTip.classList.remove('vis'); }

Eventos del hotspot:

el.addEventListener('mouseenter', () => showTip(el));
el.addEventListener('mouseleave', () => hideTip());
// Mobile: mostrar al tocar, ocultar 2 segundos después
el.addEventListener('touchstart', e => { e.preventDefault(); showTip(el); }, { passive: false });
el.addEventListener('touchend',   () => setTimeout(hideTip, 2000));

SISTEMA DE HOTSPOTS PREDEFINIDOS (no interactivos)

Este es el cambio fundamental respecto a versiones anteriores. Los hotspots no se añaden por el usuario — vienen definidos directamente en el código con toda su información. El estudiante los visualiza, los explora con hover/touch, y los estudia. No hay modal de creación, no hay botón de añadir.

Cómo definir los hotspots en el código

Cada imagen tiene un array de objetos predefinidos. Las coordenadas (pctX, pctY) son porcentajes del ancho y alto de la imagen:

const HOTSPOTS = {
  imagen1: [
    {
      id:    'hs-img1-1',
      tag:   '1',                          // etiqueta visible en el círculo
      pctX:  22,                           // % desde el borde izquierdo de la imagen
      pctY:  18,                           // % desde el borde superior de la imagen
      color: '#8b2015',                    // color del círculo
      title: 'Nombre del elemento',        // título en el tooltip
      desc:  'Explicación detallada del elemento señalado, su función y relevancia.'
    },
    {
      id:    'hs-img1-2',
      tag:   '2',
      pctX:  65,
      pctY:  42,
      color: '#1d3557',
      title: 'Segundo elemento',
      desc:  'Descripción técnica o conceptual de este segundo punto de interés.'
    },
    // ... más hotspots
  ],
  imagen2: [
    // hotspots de la segunda imagen
  ]
};

Cómo determinar las coordenadas pctX / pctY

Para colocar un hotspot sobre un elemento específico de la imagen:

  • pctX: 50 = centro horizontal
  • pctY: 25 = un cuarto desde arriba
  • Estimar visualmente inspeccionando la imagen y ajustando los valores

Al inicializar la página, renderizar todos los hotspots predefinidos automáticamente:

function initHotspots(key) {
  HOTSPOTS[key].forEach(hs => renderHS(key, hs));
  renderLegend(key);
}
// Llamar al cargar:
initHotspots('imagen1');
initHotspots('imagen2');

Leyenda automática bajo cada imagen

Bajo cada imagen se renderiza automáticamente una leyenda con todos sus hotspots:

● [color] [tag]  [Título del elemento]
                 Descripción del elemento señalado

No hay botones de añadir ni limpiar. El estudiante solo explora y lee.


ESTRUCTURA DE NAVEGACIÓN

Pestañas sticky en la parte superior, una por cada sección temática más el formulario:

[ Tab 1: Nombre Sección 1 ] [ Tab 2: Nombre Sección 2 ] [ Tab 3: Formulario ]

Comportamiento:

  • Solo una pestaña activa a la vez
  • Al cambiar de pestaña, hacer scroll al tope (window.scrollTo({ top:0, behavior:'smooth' }))
  • La barra de tabs es position: sticky; top: 0; z-index: 200

ESTRUCTURA DE CONTENIDO POR SECCIÓN

Cada sección temática incluye los siguientes bloques en este orden:

  1. Cabecera del protagonista — nombre grande, fechas, tagline en cursiva
  2. Póster o imagen de análisis — con hotspots predefinidos y leyenda automática
  3. Perfil histórico — texto en prosa, con citas destacadas en bloque visual lateral
  4. Características o conceptos clave — lista con guiones largos (—) como viñeta
  5. Línea de tiempo desplegable — cada evento se expande/colapsa con clic, animación max-height
  6. Bloque adicional temático — (virtudes, principios, categorías, etc. según el contenido)

FORMULARIO EDUCATIVO

Datos del estudiante

Grid de 2 columnas (1 en móvil):

  • Nombre completo (obligatorio*)
  • Correo electrónico (obligatorio*, validar con regex)
  • Grupo / Sección
  • Fecha

Barra de progreso

Encima del formulario, actualizada en tiempo real con cada input/change:

// Contar secciones completadas / total
const pct = Math.round(done / TOTAL * 100);
document.getElementById('prog-fill').style.width = pct + '%';

Tipos de preguntas

Incluir siempre una combinación de:

Tipo Implementación
Selección única <input type="radio"> con opciones en cards clicables
Selección múltiple <input type="checkbox"> con opciones en cards
Escala Likert 1–5 Radio buttons numerados con etiquetas en los extremos
Pregunta abierta <textarea> con mínimo de caracteres obligatorio
Pregunta de análisis crítico Bloque visualmente diferenciado (fondo oscuro), textarea largo

Estilo de opciones clicables:

.opt-lbl {
  display: flex; align-items: flex-start; gap: .6rem;
  padding: .55rem .85rem;
  border: 1px solid var(--rule);
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.opt-lbl:has(input:checked) { border-color: var(--acento); background: color-mix(in srgb, var(--acento) 8%, white); }

Validación antes de envío

Verificar en orden:

  1. Nombre no vacío
  2. Correo con regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  3. Cada radio/checkbox tenga al menos una selección
  4. Cada textarea alcance su mínimo de caracteres
  5. Si hay errores: mostrar mensajes debajo del campo, borde rojo, scroll al primer error

Envío dual con FormSubmit

const fd = new FormData();
// Añadir todos los campos del formulario
fd.append('nombre',    nombre);
fd.append('correo',    correo);
// ... resto de campos ...
fd.append('_subject',  `[Simulador] ${titulo} — ${nombre}`);
fd.append('_cc',       correo);         // copia automática al estudiante
fd.append('_captcha',  'false');
fd.append('_template', 'table');

await fetch('https://formsubmit.co/ajax/CORREO_DOCENTE@institucion.edu', {
  method: 'POST',
  body: fd,
  headers: { Accept: 'application/json' }
});

Mostrar estados: loading (spinner) → ok (confirmación verde) → fail (error rojo).


ESTÉTICA Y DISEÑO

Adaptar la paleta, tipografía y ornamentos al período histórico o temático del contenido. Ejemplos:

Período / Tema Fuentes sugeridas Paleta Ornamentos
Siglo XVIII / Clasicismo Playfair Display, IM Fell English, Libre Baskerville Pergamino, tinta, lacre, azul Prusia , reglas dobles, letra capitular
Bauhaus / Modernismo Bebas Neue, Space Mono, DM Sans Blanco, negro, rojo primario Líneas geométricas, grid visible
Art Nouveau Cinzel Decorative, Cormorant Garamond Verde salvia, dorado, marfil Flores, curvas orgánicas
Tipografía digital JetBrains Mono, Syne, Inter Negro profundo, verde terminal, gris //, { }, líneas de código

Reglas de diseño invariables:

  • Nunca usar Arial, Roboto, Inter como fuente principal
  • Nunca usar gradiente púrpura sobre blanco
  • CSS variables para toda la paleta (--nombre: valor)
  • clamp() en todos los tamaños de fuente y espaciados
  • Sombras sutiles con box-shadow, no filter: drop-shadow
  • Fondos con textura visual: radial-gradient superpuestos, no colores planos

RESPONSIVIDAD OBLIGATORIA

/* Breakpoint principal */
@media (max-width: 600px) {
  .fields-grid    { grid-template-columns: 1fr; }
  .virtues-grid   { grid-template-columns: 1fr; }
  .block-body     { padding: 1rem; }
}
  • Fuentes: font-size: clamp(mín, preferido, máx) en todos los títulos
  • Imágenes: width: 100%; height: auto
  • Botones en filas: flex-wrap: wrap
  • Hotspots: tamaño fijo en px, no escalado, para mantener legibilidad del label
  • Touch events en todos los elementos interactivos

ARCHIVOS NECESARIOS

El simulador funciona con esta estructura de carpeta:

📁 simulador/
   index.html              ← el archivo que se crea
   [imagen-1].[ext]        ← referenciada con src relativo
   [imagen-2].[ext]        ← referenciada con src relativo

Las imágenes se referencian con nombre y extensión exactos en el atributo src:

<img src="nombre-exacto-del-archivo.png" alt="Descripción">

CHECKLIST DE VERIFICACIÓN

Antes de entregar el código, confirmar que:

  • [ ] Los .hs son hijos de .poster-stage, NO de .poster-img-wrap
  • [ ] #g-tip fue creado con createElement e insertado en document.body
  • [ ] #g-tip usa position:fixed en su CSS
  • [ ] showTip() llama a getBoundingClientRect() en cada hover (no una sola vez)
  • [ ] Los hotspots están predefinidos en el código con sus coordenadas y descripciones completas
  • [ ] No existe modal de "añadir hotspot" — los puntos son parte del contenido, no del usuario
  • [ ] La leyenda se renderiza automáticamente al cargar la página
  • [ ] El formulario valida todos los campos antes de permitir envío
  • [ ] El FormData incluye _cc con el correo del estudiante
  • [ ] Las imágenes se referencian con src relativo (sin rutas absolutas)
  • [ ] ResizeObserver reposiciona los hotspots en pantallas responsive
  • [ ] Todo el contenido educativo (textos, línea de tiempo, características) está completo

Comentarios

Entradas populares de este blog

Diseños Institucionales con Adobe Illustrator: Uso de Data Merge y Scripts

Compaginación de Documentos en Adobe InDesign para Impresión Profesional

Creación y Configuración de Áreas de Sangrado en Adobe Illustrator para Diseño de Etiquetas y Packaging