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 horizontalpctY: 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:
- Cabecera del protagonista — nombre grande, fechas, tagline en cursiva
- Póster o imagen de análisis — con hotspots predefinidos y leyenda automática
- Perfil histórico — texto en prosa, con citas destacadas en bloque visual lateral
- Características o conceptos clave — lista con guiones largos (—) como viñeta
- Línea de tiempo desplegable — cada evento se expande/colapsa con clic, animación
max-height - 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:
- Nombre no vacío
- Correo con regex
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ - Cada radio/checkbox tenga al menos una selección
- Cada textarea alcance su mínimo de caracteres
- 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, nofilter: drop-shadow - Fondos con textura visual:
radial-gradientsuperpuestos, 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
.hsson hijos de.poster-stage, NO de.poster-img-wrap - [ ]
#g-tipfue creado concreateElemente insertado endocument.body - [ ]
#g-tipusaposition:fixeden su CSS - [ ]
showTip()llama agetBoundingClientRect()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
_cccon el correo del estudiante - [ ] Las imágenes se referencian con
srcrelativo (sin rutas absolutas) - [ ]
ResizeObserverreposiciona los hotspots en pantallas responsive - [ ] Todo el contenido educativo (textos, línea de tiempo, características) está completo
Comentarios
Publicar un comentario