Enlaces a las aplicaciones html
EmocionArte. Elaborado con Canvas de Gemini
https://disanedu.linexedu.eu/aplicaciones/inteligencia/infantil/emocionarte.html (auditivos)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/emociones/emocionarte1.html (con imágenes)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/emociones/emocionarte-barrido.html (con barrido para motóricos)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/emociones/emocionarte-tea.html (TEA)
Descubre las emociones. Elaborado con Claude
https://disanedu.linexedu.eu/aplicaciones/inteligencia/infantil/emociones.html (auditivos)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/infantil/emociones1.html (con imágenes)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/emociones/emociones-barrido.html (con barrido para motóricos)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/emociones/emociones-tea.html (TEA)
El barrido se selecciona en los ordenadores con la barra espaciadora o pulsador. En móviles y tablets al pulsar cualquier parte de la pantalla.
Aplicación html en un Site
Código html emocionArte
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EmocionArte: Emociones para Todos</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap');
body {
font-family: 'Nunito', sans-serif;
background-color: #f0fdf4;
user-select: none;
overflow-x: hidden;
}
.fade-in { animation: fadeIn 0.5s ease-in-out forwards; }
.bounce { animation: bounce 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.shake { animation: shake 0.4s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes bounce {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
50% { transform: translateX(10px); }
75% { transform: translateX(-10px); }
}
.memory-card {
perspective: 1000px;
cursor: pointer;
aspect-ratio: 1;
}
.memory-card-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
transform-style: preserve-3d;
}
.memory-card.flipped .memory-card-inner {
transform: rotateY(180deg);
}
.memory-card-front, .memory-card-back {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.memory-card-front {
background-color: #3b82f6;
background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,0.1) 10px, rgba(255,255,255,0.1) 20px);
font-size: 3rem;
color: white;
}
.memory-card-back {
background-color: white;
transform: rotateY(180deg);
border: 4px solid #e5e7eb;
}
#svg-container {
pointer-events: none;
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 10;
}
path.connection-line {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: drawLine 0.5s ease forwards;
}
@keyframes drawLine { to { stroke-dashoffset: 0; } }
.btn-interactive {
transition: all 0.2s;
box-shadow: 0 4px 0px 0px rgba(0,0,0,0.15);
}
.btn-interactive:active {
transform: translateY(4px);
box-shadow: 0 0px 0px 0px rgba(0,0,0,0.15);
}
.confetti {
position: fixed;
width: 10px;
height: 10px;
background-color: #f00;
z-index: 1000;
animation: fall linear forwards;
}
@keyframes fall {
to { transform: translateY(100vh) rotate(720deg); }
}
</style>
</head>
<body class="text-gray-800 h-screen w-screen overflow-hidden flex flex-col">
<header class="bg-white shadow-md p-4 flex justify-between items-center z-20 relative">
<div class="flex items-center gap-3">
<div class="bg-blue-500 text-white rounded-full w-12 h-12 flex items-center justify-center text-2xl shadow-lg">
<i class="fas fa-heart"></i>
</div>
<h1 class="text-2xl md:text-3xl font-bold text-blue-600">EmocionArte</h1>
</div>
<div class="flex items-center gap-4">
<button id="btn-back" class="hidden text-gray-500 hover:text-blue-500 font-bold p-2 transition-colors">
<i class="fas fa-home text-2xl"></i>
</button>
<button id="btn-sound" class="text-blue-500 text-2xl p-2 transition-colors">
<i class="fas fa-volume-up"></i>
</button>
</div>
</header>
<main class="flex-grow relative overflow-y-auto">
<section id="screen-menu" class="absolute inset-0 flex flex-col items-center justify-center p-4 fade-in bg-green-50">
<h2 class="text-3xl md:text-5xl font-extrabold text-center text-gray-700 mb-8 max-w-3xl">
¡Hola! ¿A qué jugamos hoy?
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-4xl">
<button onclick="app.openConfig('identify')" class="btn-interactive bg-yellow-400 hover:bg-yellow-300 rounded-3xl p-6 flex flex-col items-center justify-center gap-4 border-4 border-yellow-500">
<span class="text-6xl">🤔</span>
<span class="text-2xl font-bold text-yellow-900">Identificar</span>
</button>
<button onclick="app.openConfig('match')" class="btn-interactive bg-green-400 hover:bg-green-300 rounded-3xl p-6 flex flex-col items-center justify-center gap-4 border-4 border-green-500">
<span class="text-6xl">🔗</span>
<span class="text-2xl font-bold text-green-900">Asociar</span>
</button>
<button onclick="app.openConfig('memory')" class="btn-interactive bg-blue-400 hover:bg-blue-300 rounded-3xl p-6 flex flex-col items-center justify-center gap-4 border-4 border-blue-500">
<span class="text-6xl">🃏</span>
<span class="text-2xl font-bold text-blue-900">Parejas</span>
</button>
<button onclick="app.openConfig('write')" class="btn-interactive bg-purple-400 hover:bg-purple-300 rounded-3xl p-6 flex flex-col items-center justify-center gap-4 border-4 border-purple-500">
<span class="text-6xl">✍️</span>
<span class="text-2xl font-bold text-purple-900">Escribir</span>
</button>
</div>
</section>
<section id="screen-config" class="hidden absolute inset-0 bg-black/60 flex items-center justify-center p-4 z-50 fade-in">
<div class="bg-white rounded-3xl p-6 md:p-8 w-full max-w-2xl max-h-full overflow-y-auto shadow-2xl relative">
<button onclick="app.closeConfig()" class="absolute top-4 right-4 text-gray-400 hover:text-red-500 text-3xl">
<i class="fas fa-times-circle"></i>
</button>
<h2 class="text-3xl font-bold text-center text-blue-600 mb-6">Configurar Partida</h2>
<div class="mb-6">
<h3 class="text-xl font-bold text-gray-700 mb-3"><i class="fas fa-layer-group text-blue-500 mr-2"></i>1. Elige el Nivel</h3>
<div class="flex gap-4">
<label class="flex-1 cursor-pointer">
<input type="radio" name="level" value="1" class="peer hidden" checked>
<div class="text-center p-3 rounded-xl border-4 border-gray-200 peer-checked:border-blue-500 peer-checked:bg-blue-50 font-bold text-gray-600 transition-colors">Nivel 1</div>
</label>
<label class="flex-1 cursor-pointer">
<input type="radio" name="level" value="2" class="peer hidden">
<div class="text-center p-3 rounded-xl border-4 border-gray-200 peer-checked:border-blue-500 peer-checked:bg-blue-50 font-bold text-gray-600 transition-colors">Nivel 2</div>
</label>
<label class="flex-1 cursor-pointer">
<input type="radio" name="level" value="3" class="peer hidden">
<div class="text-center p-3 rounded-xl border-4 border-gray-200 peer-checked:border-blue-500 peer-checked:bg-blue-50 font-bold text-gray-600 transition-colors">Nivel 3</div>
</label>
</div>
</div>
<div class="mb-8">
<h3 class="text-xl font-bold text-gray-700 mb-1"><i class="fas fa-smile text-yellow-500 mr-2"></i>2. Elige las Emociones</h3>
<p class="text-sm text-gray-500 mb-3">Selecciona al menos 4 emociones para jugar.</p>
<div id="config-emotions-container" class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<!-- Rellenado por JS -->
</div>
</div>
<button onclick="app.applyConfigAndStart()" class="w-full btn-interactive bg-green-500 hover:bg-green-400 text-white font-bold text-2xl py-4 rounded-2xl shadow-[0_6px_0_0_#16a34a] transition-all">
¡A JUGAR! <i class="fas fa-play ml-2"></i>
</button>
</div>
</section>
<section id="screen-game" class="hidden absolute inset-0 flex flex-col p-4 md:p-8 bg-blue-50">
<div class="w-full max-w-4xl mx-auto mb-6 flex items-center justify-between bg-white p-4 rounded-2xl shadow-sm">
<span class="text-lg font-bold text-gray-600" id="game-title">Actividad</span>
<div class="flex gap-2 text-2xl text-gray-300" id="progress-stars">
<i class="fas fa-star"></i><i class="fas fa-star"></i><i class="fas fa-star"></i><i class="fas fa-star"></i><i class="fas fa-star"></i>
</div>
</div>
<div id="game-container" class="flex-grow flex flex-col items-center justify-center w-full max-w-5xl mx-auto relative">
<!-- Inyección de actividades -->
</div>
</section>
<div id="feedback-overlay" class="hidden absolute inset-0 pointer-events-none flex items-center justify-center z-40">
<div id="feedback-message" class="bg-white px-6 py-3 rounded-3xl shadow-2xl font-bold text-2xl transform scale-0 transition-transform duration-300">
<!-- Mensaje -->
</div>
</div>
<section id="screen-victory" class="hidden absolute inset-0 bg-green-50 flex flex-col items-center justify-center p-4 z-30">
<div class="text-[8rem] bounce mb-4">🏆</div>
<h2 class="text-4xl md:text-5xl font-extrabold text-green-600 text-center mb-8">
¡Lo has hecho genial!
</h2>
<button onclick="app.showScreen('screen-menu')" class="btn-interactive bg-blue-500 hover:bg-blue-400 text-white font-bold text-2xl py-4 px-12 rounded-full shadow-[0_6px_0_0_#2563eb] pointer-events-auto">
Volver al Menú
</button>
</section>
</main>
<script>
class EmocionArteApp {
constructor() {
const basePath = 'https://disanedu.linexedu.eu/aplicaciones/inteligencia/emociones/';
this.emotionsDB = [
{ id: 'alegria', name: 'ALEGRÍA', image: basePath + 'alegria.jpg' },
{ id: 'tristeza', name: 'TRISTEZA', image: basePath + 'tristeza.jpg' },
{ id: 'enfado', name: 'ENFADO', image: basePath + 'enfado.jpg' },
{ id: 'miedo', name: 'MIEDO', image: basePath + 'miedo.jpg' },
{ id: 'sorpresa', name: 'SORPRESA', image: basePath + 'sorpresa.jpg' },
{ id: 'asco', name: 'ASCO', image: basePath + 'asco.jpg' },
{ id: 'calma', name: 'CALMA', image: basePath + 'calna.jpg' }, // Nombre literal calna.jpg
{ id: 'preocupacion', name: 'PREOCUPACIÓN', image: basePath + 'preocupacion.jpg' }
];
this.soundEnabled = true;
this.pendingGameToStart = null;
this.currentGame = null;
this.currentLevel = 1;
this.selectedEmotions = [];
this.activeEmotions = [];
this.roundCount = 0;
this.roundTotal = 5;
this.mistakesInRound = 0;
this.isResolving = false;
this.memoryState = { flipped: [], matched: 0, lockBoard: false, pairs: [] };
this.matchState = { selectedWord: null, selectedFace: null, pairs: [], svgLine: null };
this.init();
}
init() {
this.setupEventListeners();
this.populateConfigEmotions();
document.getElementById('screen-config')?.classList.add('hidden');
}
setupEventListeners() {
document.getElementById('btn-sound')?.addEventListener('click', () => {
this.soundEnabled = !this.soundEnabled;
const icon = document.querySelector('#btn-sound i');
if(icon) icon.className = this.soundEnabled ? 'fas fa-volume-up' : 'fas fa-volume-mute';
});
document.getElementById('btn-back')?.addEventListener('click', () => {
this.showScreen('screen-menu');
});
}
speak(text) {
if (!this.soundEnabled) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'es-ES';
utterance.rate = 1.0;
window.speechSynthesis.speak(utterance);
}
showScreen(screenId) {
['screen-menu', 'screen-game', 'screen-victory'].forEach(id => {
document.getElementById(id)?.classList.add('hidden');
});
document.getElementById(screenId)?.classList.remove('hidden');
document.getElementById('screen-config')?.classList.add('hidden');
const btnBack = document.getElementById('btn-back');
if (screenId === 'screen-menu') {
btnBack?.classList.add('hidden');
this.currentGame = null;
this.isResolving = false;
} else if (screenId === 'screen-game') {
btnBack?.classList.remove('hidden');
}
}
populateConfigEmotions() {
const container = document.getElementById('config-emotions-container');
if(!container) return;
container.innerHTML = '';
// NOTA: Añadido onerror para que no se rompa la vista previa si las imágenes no están en el servidor aún.
this.emotionsDB.forEach(emo => {
const label = document.createElement('label');
label.className = 'cursor-pointer flex items-center gap-2 p-2 border-2 border-gray-200 rounded-lg hover:bg-gray-50';
label.innerHTML = `
<input type="checkbox" value="${emo.id}" class="emotion-checkbox w-5 h-5 text-blue-600 rounded" checked>
<img src="${emo.image}" onerror="this.onerror=null; this.src='https://placehold.co/200x200/e2e8f0/475569?text=${emo.name}';" alt="${emo.name}" class="w-8 h-8 rounded-md object-cover border border-gray-300 bg-gray-200">
<span class="font-bold text-gray-700 text-sm">${emo.name}</span>
`;
container.appendChild(label);
});
}
openConfig(gameType) {
this.pendingGameToStart = gameType;
document.getElementById('screen-config')?.classList.remove('hidden');
}
closeConfig() {
document.getElementById('screen-config')?.classList.add('hidden');
}
applyConfigAndStart() {
const levelRadio = document.querySelector('input[name="level"]:checked');
this.currentLevel = parseInt(levelRadio?.value || 1);
const checkboxes = document.querySelectorAll('.emotion-checkbox:checked');
this.selectedEmotions = Array.from(checkboxes).map(cb => cb.value);
if (this.selectedEmotions.length < 4) {
this.showFeedback('Elige al menos 4 emociones', 'text-red-500');
return;
}
this.activeEmotions = this.emotionsDB.filter(e => this.selectedEmotions.includes(e.id));
this.currentGame = this.pendingGameToStart;
this.closeConfig();
this.startCycle();
}
startCycle() {
this.roundCount = 0;
this.updateStars();
this.showScreen('screen-game');
const titles = {
'identify': 'Identifica la Emoción',
'match': 'Asocia Palabra y Cara',
'memory': 'Juego de Parejas',
'write': 'Escribe la Emoción'
};
const titleEl = document.getElementById('game-title');
if(titleEl) titleEl.innerText = titles[this.currentGame] || 'Juego';
this.nextRound();
}
updateStars() {
const stars = document.getElementById('progress-stars')?.children;
if(!stars) return;
for(let i=0; i<5; i++) {
if (i < this.roundCount) {
stars[i].classList.remove('text-gray-300');
stars[i].classList.add('text-yellow-400');
} else {
stars[i].classList.remove('text-yellow-400');
stars[i].classList.add('text-gray-300');
}
}
}
nextRound() {
if (this.roundCount >= this.roundTotal) {
this.showScreen('screen-victory');
this.speak("¡Lo has hecho genial! Has completado las actividades.");
this.throwConfetti();
return;
}
this.mistakesInRound = 0;
this.isResolving = false;
const container = document.getElementById('game-container');
if(!container) return;
container.innerHTML = '';
container.classList.remove('fade-in');
void container.offsetWidth;
container.classList.add('fade-in');
switch(this.currentGame) {
case 'identify': this.renderIdentify(container); break;
case 'match': this.renderMatch(container); break;
case 'memory': this.renderMemory(container); break;
case 'write': this.renderWrite(container); break;
}
}
showFeedback(text, textColorClass = 'text-gray-800') {
const overlay = document.getElementById('feedback-overlay');
const msgBox = document.getElementById('feedback-message');
if(!overlay || !msgBox) return;
msgBox.className = `bg-white px-6 py-3 rounded-3xl shadow-2xl font-bold text-2xl transform scale-0 transition-transform duration-300 ${textColorClass}`;
msgBox.innerText = text;
overlay.classList.remove('hidden');
setTimeout(() => msgBox.classList.replace('scale-0', 'scale-100'), 50);
setTimeout(() => {
msgBox.classList.replace('scale-100', 'scale-0');
setTimeout(() => overlay.classList.add('hidden'), 300);
}, 1500);
}
handleCorrect() {
if (this.isResolving) return;
this.isResolving = true;
const phrases = ["¡Muy bien!", "¡Genial!", "¡Fantástico!", "¡Excelente!"];
const txt = phrases[Math.floor(Math.random() * phrases.length)];
this.speak(txt);
this.showFeedback(txt, 'text-green-600');
this.throwConfetti();
this.roundCount++;
this.updateStars();
setTimeout(() => {
this.nextRound();
}, 2000); // 2 segundos de pausa estándar
}
handleIncorrect() {
if (this.isResolving) return;
this.mistakesInRound++;
const phrases = ["Inténtalo otra vez", "Prueba de nuevo", "¡Fíjate bien!"];
const txt = phrases[Math.floor(Math.random() * phrases.length)];
this.speak(txt);
if (this.currentGame === 'memory') {
return; // No mostramos cartel visual grande en memory para no tapar las cartas
}
if (this.mistakesInRound >= 3) {
this.autoResolve();
} else {
this.showFeedback(txt, 'text-red-500');
}
}
autoResolve() {
this.isResolving = true;
this.speak("¡Mira, esta era la respuesta!");
this.showFeedback("¡Mira la respuesta!", 'text-blue-500');
if (this.currentGame === 'identify') {
const btns = document.querySelectorAll('.identify-btn');
btns.forEach(btn => {
if (btn.dataset.correct === "true") {
btn.classList.replace('bg-white', 'bg-green-300');
btn.classList.add('ring-4', 'ring-green-500', 'scale-105');
} else {
btn.style.opacity = '0.3';
}
});
}
else if (this.currentGame === 'match') {
this.matchState.svgLine.innerHTML = '';
this.matchState.pairs.forEach(pair => {
const wordEl = document.querySelector(`.word-btn[data-id="${pair.id}"]`);
const faceEl = document.querySelector(`.face-btn[data-id="${pair.id}"]`);
if(wordEl && faceEl) {
wordEl.classList.add('bg-green-100', 'border-green-500', 'text-green-700');
faceEl.classList.add('bg-green-100', 'border-green-500');
this.drawConnectionLine(wordEl, faceEl);
}
});
}
else if (this.currentGame === 'write') {
const targetWord = this.writeState.word;
const slots = document.querySelectorAll('.letter-slot');
slots.forEach((slot, idx) => {
slot.innerText = targetWord[idx];
slot.classList.add('bg-green-100', 'border-green-500', 'text-green-700');
});
const pool = document.getElementById('letter-pool');
if(pool) pool.style.opacity = '0.2';
const delBtn = document.getElementById('btn-delete');
if(delBtn) delBtn.style.display = 'none';
}
// Espera 2 segundos exactos mostrando la solución
setTimeout(() => {
this.isResolving = false;
this.roundCount++;
this.updateStars();
this.nextRound();
}, 2000);
}
renderIdentify(container) {
const emotion = this.getRandomItem(this.activeEmotions);
let numOptions = 2;
if(this.currentLevel === 2) numOptions = 3;
if(this.currentLevel === 3) numOptions = 4;
let options = [emotion];
while(options.length < numOptions) {
const rnd = this.getRandomItem(this.activeEmotions);
if(!options.find(o => o.id === rnd.id)) options.push(rnd);
}
this.shuffleArray(options);
const html = `
<div class="text-center w-full max-w-2xl bounce">
<img src="${emotion.image}" onerror="this.onerror=null; this.src='https://placehold.co/400x400/e2e8f0/475569?text=${emotion.name}';" alt="${emotion.name}" class="w-48 h-48 sm:w-64 sm:h-64 mb-8 bg-gray-200 rounded-3xl p-2 border-4 border-gray-100 shadow-xl inline-block object-cover">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
${options.map(o => `
<button data-correct="${o.id === emotion.id}" class="identify-btn btn-interactive bg-white border-4 border-blue-200 text-3xl font-bold text-gray-700 p-6 rounded-2xl hover:bg-blue-50 focus:outline-none">
${o.name}
</button>
`).join('')}
</div>
</div>
`;
container.innerHTML = html;
container.querySelectorAll('.identify-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
if (this.isResolving) return;
const isCorrect = e.target.dataset.correct === "true";
if(isCorrect) {
e.target.classList.replace('bg-white', 'bg-green-300');
e.target.classList.add('ring-4', 'ring-green-500');
this.handleCorrect();
} else {
e.target.classList.add('shake', 'bg-red-200', 'border-red-400', 'text-red-700');
setTimeout(() => {
e.target?.classList.remove('shake', 'bg-red-200', 'border-red-400', 'text-red-700');
}, 500);
this.handleIncorrect();
}
});
});
}
renderMatch(container) {
let numPairs = 2;
if(this.currentLevel === 2) numPairs = 3;
if(this.currentLevel === 3) numPairs = 4;
let selected = [...this.activeEmotions];
this.shuffleArray(selected);
selected = selected.slice(0, numPairs);
this.matchState.pairs = selected;
this.matchState.selectedWord = null;
this.matchState.selectedFace = null;
this.matchState.matchedCount = 0;
let words = [...selected];
let faces = [...selected];
this.shuffleArray(words);
this.shuffleArray(faces);
const html = `
<div class="relative w-full max-w-4xl flex justify-between px-4 sm:px-12 bounce">
<svg id="svg-container"></svg>
<div class="flex flex-col gap-6 w-1/3 z-20">
${words.map(w => `
<button data-id="${w.id}" class="word-btn btn-interactive bg-white border-4 border-gray-300 text-xl sm:text-2xl font-bold p-4 rounded-xl text-center hover:border-blue-400 transition-colors">
${w.name}
</button>
`).join('')}
</div>
<div class="flex flex-col gap-6 w-1/3 z-20">
${faces.map(f => `
<button data-id="${f.id}" class="face-btn btn-interactive bg-white border-4 border-gray-300 p-2 sm:p-4 rounded-xl text-center hover:border-blue-400 transition-colors flex items-center justify-center">
<img src="${f.image}" onerror="this.onerror=null; this.src='https://placehold.co/200x200/e2e8f0/475569?text=${f.name}';" alt="Cara" class="w-16 h-16 sm:w-24 sm:h-24 object-cover rounded-lg pointer-events-none bg-gray-200">
</button>
`).join('')}
</div>
</div>
`;
container.innerHTML = html;
this.matchState.svgLine = document.getElementById('svg-container');
const processSelection = () => {
if(!this.matchState.selectedWord || !this.matchState.selectedFace) return;
if(this.isResolving) return;
const wId = this.matchState.selectedWord.dataset.id;
const fId = this.matchState.selectedFace.dataset.id;
if(wId === fId) {
this.drawConnectionLine(this.matchState.selectedWord, this.matchState.selectedFace);
this.matchState.selectedWord.classList.add('border-green-500', 'bg-green-50', 'text-green-600', 'cursor-default');
this.matchState.selectedFace.classList.add('border-green-500', 'bg-green-50', 'cursor-default');
this.matchState.selectedWord.disabled = true;
this.matchState.selectedFace.disabled = true;
this.matchState.matchedCount++;
this.mistakesInRound = 0;
if(this.soundEnabled) {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU');
audio.play().catch(e=>{});
}
if(this.matchState.matchedCount === numPairs) {
this.handleCorrect(); // Ya incorpora los 2 segundos de pausa en su lógica interna
}
} else {
this.matchState.selectedWord.classList.add('shake', 'border-red-400', 'bg-red-50');
this.matchState.selectedFace.classList.add('shake', 'border-red-400', 'bg-red-50');
const wTemp = this.matchState.selectedWord;
const fTemp = this.matchState.selectedFace;
setTimeout(() => {
wTemp?.classList.remove('shake', 'border-red-400', 'bg-red-50', 'border-blue-500', 'ring-4', 'ring-blue-300');
fTemp?.classList.remove('shake', 'border-red-400', 'bg-red-50', 'border-blue-500', 'ring-4', 'ring-blue-300');
}, 600);
this.handleIncorrect();
}
this.matchState.selectedWord = null;
this.matchState.selectedFace = null;
};
container.querySelectorAll('.word-btn').forEach(btn => {
btn.addEventListener('click', () => {
if(btn.disabled || this.isResolving) return;
document.querySelectorAll('.word-btn').forEach(b => !b.disabled && b.classList.remove('border-blue-500', 'ring-4', 'ring-blue-300'));
btn.classList.add('border-blue-500', 'ring-4', 'ring-blue-300');
this.matchState.selectedWord = btn;
processSelection();
});
});
container.querySelectorAll('.face-btn').forEach(btn => {
btn.addEventListener('click', () => {
if(btn.disabled || this.isResolving) return;
document.querySelectorAll('.face-btn').forEach(b => !b.disabled && b.classList.remove('border-blue-500', 'ring-4', 'ring-blue-300'));
btn.classList.add('border-blue-500', 'ring-4', 'ring-blue-300');
this.matchState.selectedFace = btn;
processSelection();
});
});
}
drawConnectionLine(el1, el2) {
const svg = this.matchState.svgLine;
if(!svg) return;
const rect1 = el1.getBoundingClientRect();
const rect2 = el2.getBoundingClientRect();
const svgRect = svg.getBoundingClientRect();
const x1 = rect1.right - svgRect.left;
const y1 = rect1.top + (rect1.height / 2) - svgRect.top;
const x2 = rect2.left - svgRect.left;
const y2 = rect2.top + (rect2.height / 2) - svgRect.top;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', `M ${x1} ${y1} C ${x1 + 50} ${y1}, ${x2 - 50} ${y2}, ${x2} ${y2}`);
path.setAttribute('stroke', '#22c55e');
path.setAttribute('stroke-width', '6');
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
path.classList.add('connection-line');
svg.appendChild(path);
}
renderMemory(container) {
let numPairs = 3;
if (this.currentLevel === 2) numPairs = 4;
if (this.currentLevel === 3) numPairs = 6;
let selected = [...this.activeEmotions];
this.shuffleArray(selected);
selected = selected.slice(0, numPairs);
this.memoryState.pairs = selected;
this.memoryState.flipped = [];
this.memoryState.matched = 0;
this.memoryState.lockBoard = false;
this.memoryState.total = numPairs;
let cards = [];
selected.forEach(emo => {
cards.push({ id: emo.id, content: emo.image, name: emo.name, type: 'face' });
cards.push({ id: emo.id, content: emo.name, type: 'word' });
});
this.shuffleArray(cards);
let cols = 'grid-cols-3';
if (numPairs >= 4) cols = 'grid-cols-4';
const html = `
<div class="grid ${cols} gap-3 sm:gap-4 w-full max-w-3xl bounce p-4">
${cards.map((c, i) => `
<div class="memory-card" data-id="${c.id}" data-index="${i}">
<div class="memory-card-inner">
<div class="memory-card-front">
<i class="fas fa-question text-white/50"></i>
</div>
<div class="memory-card-back ${c.type === 'word' ? 'text-xl sm:text-3xl font-bold text-gray-700' : 'p-1'}">
${c.type === 'face' ? `<img src="${c.content}" onerror="this.onerror=null; this.src='https://placehold.co/200x200/e2e8f0/475569?text=${c.name}';" class="w-full h-full object-cover rounded-lg pointer-events-none bg-gray-200">` : c.content}
</div>
</div>
</div>
`).join('')}
</div>
`;
container.innerHTML = html;
container.querySelectorAll('.memory-card').forEach(card => {
card.addEventListener('click', () => {
const st = this.memoryState;
if (st.lockBoard || card.classList.contains('flipped') || this.isResolving) return;
card.classList.add('flipped');
st.flipped.push(card);
if (st.flipped.length === 2) {
this.checkMemoryMatch();
}
});
});
}
checkMemoryMatch() {
const st = this.memoryState;
const [c1, c2] = st.flipped;
if(!c1 || !c2) return;
const isMatch = c1.dataset.id === c2.dataset.id;
st.lockBoard = true;
if (isMatch) {
c1.querySelector('.memory-card-back')?.classList.add('ring-4', 'ring-green-400');
c2.querySelector('.memory-card-back')?.classList.add('ring-4', 'ring-green-400');
st.matched++;
this.mistakesInRound = 0;
setTimeout(() => {
c1.classList.add('opacity-0', 'pointer-events-none', 'transition-opacity', 'duration-500');
c2.classList.add('opacity-0', 'pointer-events-none', 'transition-opacity', 'duration-500');
st.flipped = [];
if (st.matched === st.total) {
setTimeout(() => {
this.handleCorrect();
}, 500); // 0.5s extra para la animación de desvanecer
} else {
st.lockBoard = false;
}
}, 2000); // 2 segundos visibles antes de desvanecer
} else {
this.handleIncorrect();
setTimeout(() => {
c1?.classList.remove('flipped');
c2?.classList.remove('flipped');
st.flipped = [];
st.lockBoard = false;
}, 2000); // 2 segundos visibles antes de girar
}
}
renderWrite(container) {
const emotion = this.getRandomItem(this.activeEmotions);
const word = emotion.name;
let letters = word.split('');
const alphabet = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ";
let numDecoys = 0;
if(this.currentLevel === 2) numDecoys = 3;
if(this.currentLevel === 3) numDecoys = 5;
for(let i=0; i<numDecoys; i++){
letters.push(alphabet[Math.floor(Math.random() * alphabet.length)]);
}
this.shuffleArray(letters);
this.writeState = {
word: word,
currentIdx: 0,
slots: Array(word.length).fill(null),
buttons: []
};
const html = `
<div class="flex flex-col items-center w-full max-w-4xl bounce">
<img src="${emotion.image}" onerror="this.onerror=null; this.src='https://placehold.co/400x400/e2e8f0/475569?text=${emotion.name}';" alt="Emoción" class="w-40 h-40 sm:w-56 sm:h-56 mb-8 bg-gray-200 rounded-3xl p-2 shadow-lg inline-block object-cover border-4 border-gray-100">
<div class="flex items-center gap-4 mb-8 bg-white p-4 sm:p-6 rounded-2xl shadow-inner border-2 border-gray-100">
<div id="word-slots" class="flex flex-wrap justify-center gap-2 sm:gap-4">
${Array(word.length).fill(0).map(() => `
<div class="letter-slot w-12 h-14 sm:w-16 sm:h-20 border-b-8 border-gray-300 rounded-lg text-3xl sm:text-5xl font-bold flex items-center justify-center text-blue-600 transition-colors">
</div>
`).join('')}
</div>
<button id="btn-delete" class="ml-4 btn-interactive bg-red-100 hover:bg-red-200 text-red-600 border-4 border-red-300 rounded-xl p-3 sm:p-4 h-full flex items-center justify-center shadow-sm disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fas fa-backspace text-2xl sm:text-4xl pointer-events-none"></i>
</button>
</div>
<div id="letter-pool" class="flex flex-wrap justify-center gap-3 p-4 bg-gray-50 rounded-2xl w-full max-w-3xl">
${letters.map((L, i) => `
<button data-letter="${L}" id="letter-btn-${i}" class="pool-btn btn-interactive bg-white border-4 border-blue-300 text-2xl sm:text-4xl font-bold w-12 h-14 sm:w-16 sm:h-20 rounded-xl hover:bg-blue-100 text-gray-700 focus:outline-none">
${L}
</button>
`).join('')}
</div>
</div>
`;
container.innerHTML = html;
const updateSlotsUI = () => {
const slots = document.querySelectorAll('.letter-slot');
slots.forEach((slot, idx) => {
const btnData = this.writeState.slots[idx];
if (btnData) {
slot.innerText = btnData.letter;
slot.classList.replace('border-gray-300', 'border-blue-500');
slot.classList.add('bg-blue-50');
} else {
slot.innerText = '';
slot.classList.replace('border-blue-500', 'border-gray-300');
slot.classList.remove('bg-blue-50', 'bg-red-100', 'border-red-500', 'text-red-600');
}
});
const delBtn = document.getElementById('btn-delete');
if(delBtn) delBtn.disabled = this.writeState.currentIdx === 0;
};
const checkWord = () => {
if (this.isResolving) return;
const currentWord = this.writeState.slots.map(s => s ? s.letter : '').join('');
if (currentWord === this.writeState.word) {
const slots = document.querySelectorAll('.letter-slot');
slots.forEach(slot => {
slot.classList.remove('border-blue-500', 'text-blue-600', 'bg-blue-50');
slot.classList.add('border-green-500', 'text-green-600', 'bg-green-100');
});
this.handleCorrect();
} else {
const slots = document.querySelectorAll('.letter-slot');
slots.forEach(slot => {
slot.classList.add('shake', 'bg-red-100', 'border-red-500', 'text-red-600');
});
this.handleIncorrect();
if(!this.isResolving) {
setTimeout(() => {
this.writeState.slots.forEach(s => {
if(s) document.getElementById(s.id)?.classList.remove('hidden');
});
this.writeState.slots.fill(null);
this.writeState.currentIdx = 0;
updateSlotsUI();
}, 1500);
}
}
};
container.querySelectorAll('.pool-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (this.isResolving) return;
if (this.writeState.currentIdx < this.writeState.word.length) {
this.writeState.slots[this.writeState.currentIdx] = {
letter: btn.dataset.letter,
id: btn.id
};
btn.classList.add('hidden');
this.writeState.currentIdx++;
updateSlotsUI();
if (this.writeState.currentIdx === this.writeState.word.length) {
checkWord();
}
}
});
});
const delBtn = document.getElementById('btn-delete');
if(delBtn) {
delBtn.disabled = true;
delBtn.addEventListener('click', () => {
if (this.isResolving || this.writeState.currentIdx === 0) return;
const lastIdx = this.writeState.currentIdx - 1;
const removedData = this.writeState.slots[lastIdx];
if(removedData) {
document.getElementById(removedData.id)?.classList.remove('hidden');
this.writeState.slots[lastIdx] = null;
this.writeState.currentIdx--;
updateSlotsUI();
}
});
}
}
getRandomItem(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
throwConfetti() {
for(let i=0; i<50; i++) {
const conf = document.createElement('div');
conf.className = 'confetti';
conf.style.left = Math.random() * 100 + 'vw';
conf.style.top = -10 + 'px';
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
conf.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
conf.style.animationDuration = (Math.random() * 2 + 2) + 's';
conf.style.animationDelay = (Math.random() * 1) + 's';
document.body.appendChild(conf);
setTimeout(() => {
conf.remove();
}, 4000);
}
}
}
// Iniciar app
const app = new EmocionArteApp();
</script>
</body>
</html>
Código html Descubre las emociones
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<title>¡Descubre las Emociones!</title>
<style>
:root{
--bg1:#EAF6FF; --bg2:#FFF4E0;
--primary:#5B7FFF; --primary-dark:#3D5FE0;
--happy:#FFD93D; --sad:#6FA8DC; --angry:#FF6B6B; --fear:#B39DDB;
--surprise:#FFB74D; --disgust:#81C784; --calm:#4DD0E1;
--ok:#4CAF50; --bad:#E53935; --ink:#2B2B3A; --card:#ffffff;
--radius:22px; --shadow:0 8px 24px rgba(60,70,120,.18);
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
html,body{height:100%;}
body{
margin:0; font-family:'Segoe UI', Verdana, Arial, sans-serif;
background:linear-gradient(160deg,var(--bg1),var(--bg2));
color:var(--ink); min-height:100vh; overflow-x:hidden;
user-select:none;
}
#app{max-width:900px; margin:0 auto; padding:12px; min-height:100vh; display:flex; flex-direction:column;}
header{
display:flex; align-items:center; justify-content:space-between;
gap:8px; padding:10px 14px; background:var(--card); border-radius:var(--radius);
box-shadow:var(--shadow); flex-wrap:wrap;
}
header h1{font-size:1.15rem; margin:0; color:var(--primary-dark); display:flex; align-items:center; gap:8px;}
.icon-btn{
border:none; background:#EEF1FF; color:var(--primary-dark); border-radius:14px;
width:46px; height:46px; font-size:1.3rem; cursor:pointer; display:flex; align-items:center; justify-content:center;
box-shadow:0 3px 0 #d3d9ff; transition:transform .1s;
}
.icon-btn:active{transform:translateY(2px); box-shadow:0 1px 0 #d3d9ff;}
.header-controls{display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
select#difficultySelect{
border-radius:14px; border:2px solid #D9DEFF; padding:8px 10px; font-size:.95rem;
background:#fff; color:var(--ink); font-weight:600;
}
main{flex:1; display:flex; flex-direction:column; padding-top:14px;}
.screen{display:none; flex:1; flex-direction:column;}
.screen.active{display:flex;}
/* MENU */
#menuScreen h2{text-align:center; color:var(--primary-dark); font-size:1.4rem; margin:6px 0 16px;}
.menu-grid{display:grid; grid-template-columns:1fr 1fr; gap:16px;}
@media (max-width:520px){.menu-grid{grid-template-columns:1fr;}}
.menu-card{
background:var(--card); border-radius:var(--radius); box-shadow:var(--shadow);
padding:20px 14px; text-align:center; cursor:pointer; border:none;
display:flex; flex-direction:column; align-items:center; gap:10px;
font-family:inherit; font-size:1.05rem; font-weight:700; color:var(--ink);
transition:transform .12s;
}
.menu-card:active{transform:scale(.96);}
.menu-card .emoji{font-size:2.6rem;}
.menu-card small{font-weight:400; color:#666; font-size:.85rem;}
.menu-card.c1{border-bottom:6px solid var(--happy);}
.menu-card.c2{border-bottom:6px solid var(--surprise);}
.menu-card.c3{border-bottom:6px solid var(--calm);}
.menu-card.c4{border-bottom:6px solid var(--fear);}
/* GAME HEADER BAR */
.game-bar{
display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; gap:8px; flex-wrap:wrap;
}
.badge{
background:var(--card); border-radius:16px; box-shadow:var(--shadow); padding:8px 14px;
font-weight:700; font-size:.95rem; display:flex; align-items:center; gap:6px;
}
.streak-badge{color:#E67E22;}
.score-badge{color:var(--ok);}
.game-area{flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; gap:18px;}
/* FACE CARD */
.face-wrap{
background:var(--card); border-radius:28px; box-shadow:var(--shadow); padding:18px;
display:flex; align-items:center; justify-content:center;
}
.face-wrap svg{width:min(46vw,220px); height:min(46vw,220px); display:block;}
.prompt-word{
background:var(--card); border-radius:28px; box-shadow:var(--shadow);
padding:22px 34px; font-size:2rem; font-weight:800; color:var(--primary-dark); text-align:center;
}
/* OPTIONS */
.options-grid{
display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:12px; width:100%; max-width:700px;
}
.opt-btn{
background:var(--card); border:3px solid #E3E7FF; border-radius:18px; box-shadow:var(--shadow);
padding:14px 10px; font-size:1.05rem; font-weight:700; cursor:pointer; color:var(--ink);
display:flex; flex-direction:column; align-items:center; gap:8px; font-family:inherit;
min-height:56px; transition:transform .1s, background .2s, border-color .2s;
}
.opt-btn:active{transform:scale(.95);}
.opt-btn.face-opt svg{width:64px; height:64px;}
.opt-btn.correct{background:#E3F8E5; border-color:var(--ok); animation:pop .35s;}
.opt-btn.wrong{background:#FFE6E4; border-color:var(--bad); animation:shake .35s;}
.opt-btn[disabled]{cursor:default;}
@keyframes pop{0%{transform:scale(1);}50%{transform:scale(1.12);}100%{transform:scale(1);}}
@keyframes shake{0%,100%{transform:translateX(0);}25%{transform:translateX(-8px);}75%{transform:translateX(8px);}}
.feedback-msg{
font-size:1.3rem; font-weight:800; min-height:34px; text-align:center;
}
.feedback-msg.ok{color:var(--ok);}
.feedback-msg.bad{color:var(--bad);}
.cartel{
display:none; margin:0 auto; text-align:center; font-size:1.6rem; font-weight:900; color:#fff;
padding:12px 30px; border-radius:16px; letter-spacing:.5px;
background:linear-gradient(135deg,#4CAF50,#2E9E4F);
box-shadow:0 10px 24px rgba(46,158,79,.4);
border:4px solid #fff;
position:relative;
}
.cartel::before, .cartel::after{
content:''; position:absolute; top:-10px; width:16px; height:16px; border-radius:50%;
background:#D8A24A; box-shadow:0 2px 3px rgba(0,0,0,.3);
}
.cartel::before{left:14px;}
.cartel::after{right:14px;}
.cartel.show{display:block; animation:cartelPop .5s ease;}
@keyframes cartelPop{
0%{transform:scale(.3) rotate(-6deg); opacity:0;}
60%{transform:scale(1.12) rotate(2deg); opacity:1;}
100%{transform:scale(1) rotate(0deg); opacity:1;}
}
/* MEMORY */
.memory-grid{
display:grid; gap:10px; width:100%; max-width:640px;
grid-template-columns:repeat(4,1fr);
}
@media (max-width:480px){.memory-grid{grid-template-columns:repeat(3,1fr);}}
.mem-card{
aspect-ratio:1/1; border-radius:16px; background:var(--primary); box-shadow:var(--shadow);
display:flex; align-items:center; justify-content:center; cursor:pointer; position:relative;
font-size:1.8rem; color:#fff;
}
.mem-card .mem-inner{display:none; width:100%; height:100%; align-items:center; justify-content:center; flex-direction:column; padding:6px;}
.mem-card .mem-inner svg{width:70%; height:70%;}
.mem-card .mem-inner span{font-size:.8rem; font-weight:800; color:var(--ink); text-align:center;}
.mem-card.flipped{background:#fff;}
.mem-card.flipped .mem-back{display:none;}
.mem-card.flipped .mem-inner{display:flex;}
.mem-card.matched{background:#E3F8E5; opacity:.85; cursor:default;}
.mem-card.wrong-flash{animation:shake .35s;}
/* ESCRIBIR */
#writeInput{
font-size:1.4rem; padding:12px 16px; border-radius:16px; border:3px solid #E3E7FF;
width:min(90vw,320px); text-align:center; font-family:inherit; font-weight:700; color:var(--ink);
}
#writeInput:focus{outline:none; border-color:var(--primary);}
.kb{display:grid; grid-template-columns:repeat(9,1fr); gap:6px; width:100%; max-width:620px;}
.kb button{
padding:12px 0; border-radius:10px; border:none; background:var(--card); box-shadow:0 3px 0 #ddd;
font-weight:700; font-size:1rem; cursor:pointer; color:var(--ink);
}
.kb button:active{transform:translateY(2px); box-shadow:none;}
.kb button.wide{grid-column:span 3;}
.next-btn, .primary-btn{
background:var(--primary); color:#fff; border:none; border-radius:18px; padding:14px 26px;
font-size:1.1rem; font-weight:800; cursor:pointer; box-shadow:0 4px 0 var(--primary-dark);
font-family:inherit;
}
.next-btn:active, .primary-btn:active{transform:translateY(3px); box-shadow:none;}
.next-btn[hidden]{display:none;}
/* MOTIVATION OVERLAY */
#motivationOverlay{
position:fixed; inset:0; background:rgba(40,40,70,.55); display:none; align-items:center; justify-content:center;
z-index:50; padding:20px;
}
#motivationOverlay.show{display:flex;}
.motiv-card{
background:#fff; border-radius:28px; padding:34px 26px; text-align:center; max-width:360px;
box-shadow:0 20px 50px rgba(0,0,0,.3); animation:pop .4s;
}
.motiv-card .big-emoji{font-size:3.6rem; display:block; margin-bottom:10px; animation:bounce 1s infinite;}
.motiv-card h3{margin:0 0 8px; color:var(--primary-dark); font-size:1.4rem;}
@keyframes bounce{0%,100%{transform:translateY(0);}50%{transform:translateY(-14px);}}
.confetti{position:absolute; top:-10px; border-radius:2px; animation:fall linear forwards;}
@keyframes fall{to{transform:translateY(110vh) rotate(360deg); opacity:.2;}}
/* STATS */
#statsScreen table{width:100%; border-collapse:collapse; background:#fff; border-radius:16px; overflow:hidden; box-shadow:var(--shadow);}
#statsScreen th, #statsScreen td{padding:10px 8px; text-align:center; font-size:.92rem;}
#statsScreen th{background:var(--primary); color:#fff;}
#statsScreen tr:nth-child(even){background:#F5F7FF;}
.stats-cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:10px; margin-bottom:16px;}
.stat-mini{background:#fff; border-radius:16px; box-shadow:var(--shadow); padding:14px; text-align:center;}
.stat-mini .num{font-size:1.6rem; font-weight:800; color:var(--primary-dark);}
.stat-mini .lbl{font-size:.8rem; color:#666;}
.reset-btn{margin-top:16px; background:var(--bad); color:#fff; border:none; border-radius:16px; padding:10px 18px; font-weight:700; cursor:pointer;}
footer.credit{text-align:center; font-size:.75rem; color:#999; padding:14px 0 4px;}
/* landscape tweaks */
@media (orientation:landscape) and (max-height:480px){
.face-wrap svg{width:120px; height:120px;}
.menu-grid{grid-template-columns:repeat(4,1fr);}
}
</style>
</head>
<body>
<div id="app">
<header>
<h1>🙂 Descubre las Emociones</h1>
<div class="header-controls">
<select id="difficultySelect" aria-label="Nivel de dificultad">
<option value="1">⭐ Fácil</option>
<option value="2" selected>⭐⭐ Medio</option>
<option value="3">⭐⭐⭐ Difícil</option>
</select>
<button class="icon-btn" id="soundBtn" title="Sonido">🔇</button>
<button class="icon-btn" id="statsBtn" title="Estadísticas">📊</button>
<button class="icon-btn" id="homeBtn" title="Menú" hidden>🏠</button>
</div>
</header>
<main>
<!-- MENU -->
<section id="menuScreen" class="screen active">
<h2>¿A qué quieres jugar?</h2>
<div class="menu-grid">
<button class="menu-card c1" data-game="identificar">
<span class="emoji">🔍</span> Identificar
<small>Mira la cara y elige la emoción</small>
</button>
<button class="menu-card c2" data-game="asociar">
<span class="emoji">🔗</span> Asociar
<small>Une la palabra con la cara</small>
</button>
<button class="menu-card c3" data-game="parejas">
<span class="emoji">🃏</span> Parejas
<small>Encuentra las parejas iguales</small>
</button>
<button class="menu-card c4" data-game="escribir">
<span class="emoji">✏️</span> Escribir
<small>Escribe el nombre de la emoción</small>
</button>
</div>
</section>
<!-- GAME GENERIC -->
<section id="gameScreen" class="screen">
<div class="game-bar">
<div class="badge streak-badge">🔥 Racha: <span id="streakVal">0</span></div>
<div class="badge score-badge">✅ Aciertos: <span id="scoreVal">0</span></div>
</div>
<div class="game-area" id="gameArea"></div>
<div class="cartel" id="cartelMsg"></div>
<div class="feedback-msg" id="feedbackMsg"></div>
<div style="text-align:center; margin-top:10px;">
<button class="next-btn" id="nextBtn" hidden>Siguiente ➜</button>
</div>
</section>
<!-- STATS -->
<section id="statsScreen" class="screen">
<h2 style="text-align:center; color:var(--primary-dark);">📊 Panel del docente</h2>
<div class="stats-cards" id="statsCards"></div>
<table>
<thead><tr><th>Juego</th><th>Intentos</th><th>Aciertos</th><th>% Acierto</th><th>Mejor racha</th></tr></thead>
<tbody id="statsTableBody"></tbody>
</table>
<div style="text-align:center;">
<button class="reset-btn" id="resetStatsBtn">Reiniciar estadísticas</button>
</div>
</section>
</main>
<footer class="credit">Recurso educativo · Reconocimiento de emociones · Compatible con ratón y pantalla táctil</footer>
</div>
<div id="motivationOverlay">
<div class="motiv-card">
<span class="big-emoji" id="motivEmoji">🌟</span>
<h3 id="motivMsg">¡Genial!</h3>
<button class="primary-btn" id="motivCloseBtn">Seguir jugando</button>
</div>
</div>
<script>
(function(){
"use strict";
/* ---------------- EMOTIONS & FACE DRAWING ---------------- */
const EMOTIONS = [
{id:'alegria', label:'Alegría', color:'#FFD93D'},
{id:'tristeza', label:'Tristeza', color:'#6FA8DC'},
{id:'enfado', label:'Enfado', color:'#FF6B6B'},
{id:'sorpresa', label:'Sorpresa', color:'#FFB74D'},
{id:'miedo', label:'Miedo', color:'#B39DDB'},
{id:'asco', label:'Asco', color:'#81C784'},
{id:'calma', label:'Calma', color:'#4DD0E1'}
];
const emoById = id => EMOTIONS.find(e=>e.id===id);
function faceBase(inner){
return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="88" fill="#FFE0B2" stroke="#5C4326" stroke-width="4"/>
${inner}
</svg>`;
}
function eyebrowPair(rot){ // rot: degrees, +down at inner corner style variants handled per call
return '';
}
// f = intensity factor 0.35 (subtle) .. 1 (very exaggerated)
function drawFace(emotionId, f){
f = Math.max(0.3, Math.min(1, f));
switch(emotionId){
case 'alegria': {
const curve = 22 + 26*f;
return faceBase(`
<path d="M62,72 Q78,60 94,70" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M106,70 Q122,60 138,72" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M72,96 Q80,${86-4*f} 88,96" stroke="#333" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M112,96 Q120,${86-4*f} 128,96" stroke="#333" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M60,128 Q100,${128+curve} 140,128" stroke="#5C4326" stroke-width="8" fill="none" stroke-linecap="round"/>
<circle cx="60" cy="112" r="${10*f}" fill="#FFB6A0" opacity="${0.5*f}"/>
<circle cx="140" cy="112" r="${10*f}" fill="#FFB6A0" opacity="${0.5*f}"/>
`);
}
case 'tristeza': {
const droop = 18 + 20*f;
return faceBase(`
<path d="M62,74 Q78,${84+8*f} 94,80" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M106,80 Q122,${84+8*f} 138,74" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<circle cx="80" cy="98" r="9" fill="#333"/>
<circle cx="120" cy="98" r="9" fill="#333"/>
<path d="M75,108 q5,10 0,16" stroke="#6FA8DC" stroke-width="4" fill="none" stroke-linecap="round" opacity="${0.7*f}"/>
<path d="M60,${132+droop*0.4} Q100,${132-droop} 140,${132+droop*0.4}" stroke="#5C4326" stroke-width="8" fill="none" stroke-linecap="round"/>
`);
}
case 'enfado': {
const ang = 10 + 12*f;
return faceBase(`
<line x1="62" y1="${72+ang}" x2="94" y2="70" stroke="#5C4326" stroke-width="7" stroke-linecap="round"/>
<line x1="106" y1="70" x2="138" y2="${72+ang}" stroke="#5C4326" stroke-width="7" stroke-linecap="round"/>
<ellipse cx="80" cy="98" rx="10" ry="${9-3*f}" fill="#333"/>
<ellipse cx="120" cy="98" rx="10" ry="${9-3*f}" fill="#333"/>
<path d="M65,${134+6*f} Q100,${126-4*f} 135,${134+6*f}" stroke="#5C4326" stroke-width="8" fill="none" stroke-linecap="round"/>
<circle cx="62" cy="115" r="${9*f}" fill="#FF8A80" opacity="${0.55*f}"/>
<circle cx="138" cy="115" r="${9*f}" fill="#FF8A80" opacity="${0.55*f}"/>
`);
}
case 'sorpresa': {
const r = 10 + 6*f;
return faceBase(`
<path d="M60,${64-8*f} Q78,${52-8*f} 96,${64-6*f}" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M104,${64-6*f} Q122,${52-8*f} 140,${64-8*f}" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<circle cx="80" cy="98" r="${r}" fill="#fff" stroke="#333" stroke-width="3"/>
<circle cx="120" cy="98" r="${r}" fill="#fff" stroke="#333" stroke-width="3"/>
<circle cx="80" cy="98" r="${r*0.45}" fill="#333"/>
<circle cx="120" cy="98" r="${r*0.45}" fill="#333"/>
<ellipse cx="100" cy="135" rx="${10+6*f}" ry="${12+10*f}" fill="#8B4A3B" stroke="#5C4326" stroke-width="4"/>
`);
}
case 'miedo': {
const r = 9 + 5*f;
return faceBase(`
<path d="M60,${68-6*f} Q78,${58-10*f} 98,68" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M102,68 Q122,${58-10*f} 140,${68-6*f}" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<circle cx="80" cy="98" r="${r}" fill="#fff" stroke="#333" stroke-width="3"/>
<circle cx="120" cy="98" r="${r}" fill="#fff" stroke="#333" stroke-width="3"/>
<circle cx="82" cy="100" r="${r*0.4}" fill="#333"/>
<circle cx="122" cy="100" r="${r*0.4}" fill="#333"/>
<path d="M85,132 Q100,${140+10*f} 115,132 Q100,${138+4*f} 85,132 Z" fill="#8B4A3B" stroke="#5C4326" stroke-width="3"/>
<path d="M60,110 q-8,${6*f} -2,${14*f}" stroke="#B39DDB" stroke-width="4" fill="none" stroke-linecap="round" opacity="${0.6*f}"/>
<path d="M140,110 q8,${6*f} 2,${14*f}" stroke="#B39DDB" stroke-width="4" fill="none" stroke-linecap="round" opacity="${0.6*f}"/>
`);
}
case 'asco': {
const wrinkle = 4 + 4*f;
return faceBase(`
<line x1="60" y1="76" x2="92" y2="70" stroke="#5C4326" stroke-width="6" stroke-linecap="round"/>
<path d="M108,74 Q122,${64-4*f} 136,${72+6*f}" stroke="#5C4326" stroke-width="6" fill="none" stroke-linecap="round"/>
<path d="M70,98 q10,${-6*f} 20,0" stroke="#333" stroke-width="6" fill="none" stroke-linecap="round"/>
<ellipse cx="122" cy="98" rx="9" ry="${5-2*f}" fill="#333"/>
<path d="M93,108 q7,${wrinkle} 14,0" stroke="#5C4326" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M65,132 Q85,${120+6*f} 105,130 Q120,${138+4*f} 138,126" stroke="#5C4326" stroke-width="7" fill="none" stroke-linecap="round"/>
<circle cx="150" cy="105" r="${6*f}" fill="#81C784" opacity="${0.6*f}"/>
`);
}
case 'calma': {
const smile = 6 + 10*f;
return faceBase(`
<path d="M64,74 Q80,68 96,73" stroke="#5C4326" stroke-width="5" fill="none" stroke-linecap="round"/>
<path d="M104,73 Q120,68 136,74" stroke="#5C4326" stroke-width="5" fill="none" stroke-linecap="round"/>
<path d="M70,98 Q80,${102+2*f} 90,98" stroke="#333" stroke-width="5" fill="none" stroke-linecap="round"/>
<path d="M110,98 Q120,${102+2*f} 130,98" stroke="#333" stroke-width="5" fill="none" stroke-linecap="round"/>
<path d="M72,130 Q100,${130+smile} 128,130" stroke="#5C4326" stroke-width="7" fill="none" stroke-linecap="round"/>
`);
}
default: return faceBase('');
}
}
/* ---------------- DIFFICULTY ---------------- */
const DIFF = {
1:{label:'Fácil', factor:0.95, pool:['alegria','tristeza','enfado','sorpresa'], options:3, pairs:4},
2:{label:'Medio', factor:0.7, pool:['alegria','tristeza','enfado','sorpresa','miedo','calma'], options:4, pairs:5},
3:{label:'Difícil', factor:0.42, pool:EMOTIONS.map(e=>e.id), options:6, pairs:6}
};
let currentDifficulty = 2;
/* ---------------- STATE / STORAGE ---------------- */
const STORAGE_KEY = 'emocionesCarasStats_v1';
function loadStats(){
try{
const raw = localStorage.getItem(STORAGE_KEY);
if(raw) return JSON.parse(raw);
}catch(e){}
return blankStats();
}
function blankStats(){
const games = ['identificar','asociar','parejas','escribir'];
const s = {racha:0, mejorRacha:0, porJuego:{}};
games.forEach(g=>{
s.porJuego[g] = {intentos:0, aciertos:0, mejorRacha:0, porNivel:{1:{i:0,a:0},2:{i:0,a:0},3:{i:0,a:0}}};
});
return s;
}
let stats = loadStats();
function saveStats(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(stats)); }
let soundOn = false;
let racha = 0;
let aciertosSesion = 0;
/* ---------------- SOUND (Web Audio, optional reinforcement) ---------------- */
let audioCtx = null;
function playTone(freq, startTime, duration, waveType, gainVal){
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
o.type = waveType;
o.frequency.value = freq;
g.gain.setValueAtTime(0.0001, startTime);
g.gain.exponentialRampToValueAtTime(gainVal, startTime+0.02);
g.gain.exponentialRampToValueAtTime(0.0001, startTime+duration);
o.start(startTime);
o.stop(startTime+duration+0.03);
}
function beep(type){
if(!soundOn) return;
try{
audioCtx = audioCtx || new (window.AudioContext||window.webkitAudioContext)();
const now = audioCtx.currentTime;
if(type==='ok'){
// acorde ascendente, alegre
playTone(660, now, 0.16, 'sine', 0.09);
playTone(880, now+0.12, 0.22, 'sine', 0.09);
} else {
// sonido suave y agradable (no brusco) que igualmente indica error: dos notas descendentes
playTone(392, now, 0.16, 'triangle', 0.07);
playTone(311, now+0.15, 0.28, 'triangle', 0.07);
}
}catch(e){}
}
function speak(text){
if(!soundOn) return;
try{
if('speechSynthesis' in window){
window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = 'es-ES';
u.rate = 1;
u.pitch = 1.05;
window.speechSynthesis.speak(u);
}
}catch(e){}
}
/* ---------------- HELPERS ---------------- */
function normalize(str){
return str.toLowerCase().trim().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
}
function shuffle(arr){
const a = arr.slice();
for(let i=a.length-1;i>0;i--){
const j = Math.floor(Math.random()*(i+1));
[a[i],a[j]]=[a[j],a[i]];
}
return a;
}
function pick(arr,n){ return shuffle(arr).slice(0,n); }
function randOf(arr){ return arr[Math.floor(Math.random()*arr.length)]; }
/* ---------------- UI ELEMENTS ---------------- */
const screens = {
menu: document.getElementById('menuScreen'),
game: document.getElementById('gameScreen'),
stats: document.getElementById('statsScreen')
};
const gameArea = document.getElementById('gameArea');
const cartelMsg = document.getElementById('cartelMsg');
const feedbackMsg = document.getElementById('feedbackMsg');
const POSITIVE_MSGS = ['¡Muy bien!','¡Excelente!','¡Genial!','¡Sigue así!','¡Fenomenal!'];
let cartelTimeout = null;
function showCartel(){
const msg = randOf(POSITIVE_MSGS);
cartelMsg.textContent = msg;
cartelMsg.classList.remove('show');
void cartelMsg.offsetWidth; // reinicia la animación
cartelMsg.classList.add('show');
beep('ok');
speak(msg);
clearTimeout(cartelTimeout);
cartelTimeout = setTimeout(()=> cartelMsg.classList.remove('show'), 1700);
}
const nextBtn = document.getElementById('nextBtn');
const streakVal = document.getElementById('streakVal');
const scoreVal = document.getElementById('scoreVal');
const homeBtn = document.getElementById('homeBtn');
const soundBtn = document.getElementById('soundBtn');
const statsBtn = document.getElementById('statsBtn');
const difficultySelect = document.getElementById('difficultySelect');
function showScreen(name){
Object.values(screens).forEach(s=>s.classList.remove('active'));
screens[name].classList.add('active');
homeBtn.hidden = (name==='menu');
}
homeBtn.addEventListener('click', ()=> showScreen('menu'));
statsBtn.addEventListener('click', ()=>{ renderStats(); showScreen('stats'); });
soundBtn.addEventListener('click', ()=>{
soundOn = !soundOn;
soundBtn.textContent = soundOn ? '🔊' : '🔇';
});
difficultySelect.addEventListener('change', e=>{
currentDifficulty = parseInt(e.target.value,10);
});
document.querySelectorAll('.menu-card').forEach(btn=>{
btn.addEventListener('click', ()=>{
startGame(btn.dataset.game);
});
});
/* ---------------- GAME STATE ---------------- */
let activeGame = null; // 'identificar' | 'asociar' | 'parejas' | 'escribir'
function startGame(game){
activeGame = game;
racha = 0; aciertosSesion = 0;
updateBadges();
showScreen('game');
nextGameRound();
}
function updateBadges(){
streakVal.textContent = racha;
scoreVal.textContent = aciertosSesion;
}
function nextGameRound(){
feedbackMsg.textContent = '';
feedbackMsg.className = 'feedback-msg';
cartelMsg.classList.remove('show');
clearTimeout(cartelTimeout);
nextBtn.hidden = true;
if(activeGame==='identificar') renderIdentificar();
else if(activeGame==='asociar') renderAsociar();
else if(activeGame==='parejas') renderParejas();
else if(activeGame==='escribir') renderEscribir();
}
nextBtn.addEventListener('click', nextGameRound);
function registerResult(correct){
const g = stats.porJuego[activeGame];
g.intentos++;
g.porNivel[currentDifficulty].i++;
if(correct){
g.aciertos++;
g.porNivel[currentDifficulty].a++;
racha++; aciertosSesion++;
if(racha > g.mejorRacha) g.mejorRacha = racha;
if(racha > stats.mejorRacha) stats.mejorRacha = racha;
} else {
racha = 0;
}
updateBadges();
saveStats();
if(correct && racha>0 && racha % 5 === 0) triggerMotivation();
}
/* ---------------- MOTIVATION OVERLAY ---------------- */
const MOTIV_MSGS = [
{emoji:'🌟', msg:'¡Lo estás haciendo genial!'},
{emoji:'🎉', msg:'¡5 aciertos seguidos, sigue así!'},
{emoji:'🏆', msg:'¡Eres un campeón de las emociones!'},
{emoji:'🚀', msg:'¡Vas súper bien, no pares!'},
{emoji:'💪', msg:'¡Qué fuerte eres reconociendo emociones!'}
];
const overlay = document.getElementById('motivationOverlay');
const motivEmoji = document.getElementById('motivEmoji');
const motivMsg = document.getElementById('motivMsg');
document.getElementById('motivCloseBtn').addEventListener('click', closeMotivation);
function triggerMotivation(){
const m = randOf(MOTIV_MSGS);
motivEmoji.textContent = m.emoji;
motivMsg.textContent = m.msg;
overlay.classList.add('show');
spawnConfetti();
beep('ok');
}
function closeMotivation(){
overlay.classList.remove('show');
document.querySelectorAll('.confetti').forEach(c=>c.remove());
}
function spawnConfetti(){
const colors = ['#FFD93D','#FF6B6B','#6FA8DC','#81C784','#B39DDB','#FFB74D'];
for(let i=0;i<28;i++){
const c = document.createElement('div');
c.className='confetti';
const size = 6+Math.random()*8;
c.style.width = size+'px';
c.style.height = (size*1.4)+'px';
c.style.left = Math.random()*100+'%';
c.style.background = colors[Math.floor(Math.random()*colors.length)];
c.style.animationDuration = (1.6+Math.random()*1.4)+'s';
document.body.appendChild(c);
setTimeout(()=>c.remove(), 3200);
}
}
/* ---------------- MODE 1: IDENTIFICAR (ver cara -> elegir palabra) ---------------- */
function renderIdentificar(){
const d = DIFF[currentDifficulty];
const correctId = randOf(d.pool);
const distractors = pick(d.pool.filter(id=>id!==correctId), Math.min(d.options-1, d.pool.length-1));
const options = shuffle([correctId, ...distractors]);
gameArea.innerHTML = `
<div class="face-wrap">${drawFace(correctId, d.factor)}</div>
<div class="options-grid" id="optsGrid"></div>
`;
const grid = document.getElementById('optsGrid');
options.forEach(id=>{
const b = document.createElement('button');
b.className='opt-btn';
b.textContent = emoById(id).label;
b.dataset.emotion = id;
b.addEventListener('click', ()=> handleChoice(b, id===correctId, correctId, grid));
grid.appendChild(b);
});
}
/* ---------------- MODE 2: ASOCIAR (ver palabra -> elegir cara) ---------------- */
function renderAsociar(){
const d = DIFF[currentDifficulty];
const correctId = randOf(d.pool);
const distractors = pick(d.pool.filter(id=>id!==correctId), Math.min(d.options-1, d.pool.length-1));
const options = shuffle([correctId, ...distractors]);
gameArea.innerHTML = `
<div class="prompt-word">${emoById(correctId).label}</div>
<div class="options-grid" id="optsGrid"></div>
`;
const grid = document.getElementById('optsGrid');
options.forEach(id=>{
const b = document.createElement('button');
b.className='opt-btn face-opt';
b.innerHTML = drawFace(id, d.factor);
b.dataset.emotion = id;
b.addEventListener('click', ()=> handleChoice(b, id===correctId, correctId, grid, true));
grid.appendChild(b);
});
}
function handleChoice(btn, isCorrect, correctId, grid, isFaceGrid){
grid.querySelectorAll('button').forEach(b=>b.disabled=true);
if(isCorrect){
btn.classList.add('correct');
feedbackMsg.textContent = '';
feedbackMsg.className = 'feedback-msg ok';
showCartel();
} else {
btn.classList.add('wrong');
feedbackMsg.textContent = `No era esa. La respuesta correcta es: ${emoById(correctId).label}`;
feedbackMsg.className = 'feedback-msg bad';
beep('bad');
// resaltar visualmente la opción correcta (cara o palabra)
const correctBtn = grid.querySelector(`[data-emotion="${correctId}"]`);
if(correctBtn) correctBtn.classList.add('correct');
}
registerResult(isCorrect);
nextBtn.hidden = false;
}
/* ---------------- MODE 3: PAREJAS (memory: cara <-> palabra) ---------------- */
let memState = {cards:[], flipped:[], lock:false, matched:0, total:0};
function renderParejas(){
const d = DIFF[currentDifficulty];
const chosen = pick(d.pool, Math.min(d.pairs, d.pool.length));
let cards = [];
chosen.forEach(id=>{
cards.push({emotion:id, kind:'face', key:id+'-face'});
cards.push({emotion:id, kind:'word', key:id+'-word'});
});
cards = shuffle(cards);
memState = {cards, flipped:[], lock:false, matched:0, total:chosen.length};
const grid = document.createElement('div');
grid.className='memory-grid';
cards.forEach((card,idx)=>{
const cell = document.createElement('div');
cell.className='mem-card';
cell.dataset.idx = idx;
cell.innerHTML = `
<div class="mem-back">❓</div>
<div class="mem-inner">
${card.kind==='face' ? drawFace(card.emotion, d.factor) : `<span>${emoById(card.emotion).label}</span>`}
</div>
`;
cell.addEventListener('click', ()=> flipMemCard(idx, cell));
grid.appendChild(cell);
});
gameArea.innerHTML='';
gameArea.appendChild(grid);
feedbackMsg.textContent = 'Encuentra la cara y la palabra de cada emoción';
feedbackMsg.className='feedback-msg';
}
function flipMemCard(idx, cell){
if(memState.lock) return;
if(cell.classList.contains('flipped') || cell.classList.contains('matched')) return;
cell.classList.add('flipped');
memState.flipped.push({idx, cell});
if(memState.flipped.length===2){
memState.lock = true;
const [a,b] = memState.flipped;
const ca = memState.cards[a.idx], cb = memState.cards[b.idx];
const isMatch = ca.emotion===cb.emotion && ca.kind!==cb.kind;
setTimeout(()=>{
if(isMatch){
a.cell.classList.add('matched');
b.cell.classList.add('matched');
memState.matched++;
registerResult(true);
showCartel();
if(memState.matched===memState.total){
feedbackMsg.textContent = '¡Completado! Todas las parejas encontradas 🎉';
feedbackMsg.className='feedback-msg ok';
nextBtn.hidden = false;
}
} else {
a.cell.classList.remove('flipped'); a.cell.classList.add('wrong-flash');
b.cell.classList.remove('flipped'); b.cell.classList.add('wrong-flash');
setTimeout(()=>{ a.cell.classList.remove('wrong-flash'); b.cell.classList.remove('wrong-flash'); }, 350);
beep('bad');
registerResult(false);
}
memState.flipped = [];
memState.lock = false;
}, isMatch ? 350 : 700);
}
}
/* ---------------- MODE 4: ESCRIBIR ---------------- */
function wordLetters(label){
const clean = label.toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^A-ZÑ]/g,'');
return Array.from(new Set(clean.split('')));
}
function buildKeyboardLetters(correctId, level){
const base = wordLetters(emoById(correctId).label);
const extraCount = level===1 ? 0 : (level===2 ? 3 : 6);
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(c=>!base.includes(c));
const extra = pick(alphabet, Math.min(extraCount, alphabet.length));
return shuffle([...base, ...extra]);
}
function renderEscribir(){
const d = DIFF[currentDifficulty];
const correctId = randOf(d.pool);
gameArea.innerHTML = `
<div class="face-wrap">${drawFace(correctId, d.factor)}</div>
<input type="text" id="writeInput" placeholder="Escribe la emoción..." autocomplete="off" autocorrect="off" spellcheck="false"/>
<div class="kb" id="kb"></div>
<button class="primary-btn" id="checkWriteBtn">Comprobar</button>
`;
const input = document.getElementById('writeInput');
const kb = document.getElementById('kb');
const letters = buildKeyboardLetters(correctId, currentDifficulty);
letters.forEach(l=>{
const b = document.createElement('button');
b.textContent = l;
b.addEventListener('click', ()=>{ input.value += l.toLowerCase(); input.focus(); });
kb.appendChild(b);
});
const back = document.createElement('button');
back.textContent='⌫ Borrar'; back.className='wide';
back.addEventListener('click', ()=>{ input.value = input.value.slice(0,-1); input.focus(); });
kb.appendChild(back);
function checkAnswer(){
if(input.disabled) return;
const val = normalize(input.value);
const correctVal = normalize(emoById(correctId).label);
const isCorrect = val.length>0 && val===correctVal;
input.disabled = true;
document.getElementById('checkWriteBtn').disabled = true;
kb.querySelectorAll('button').forEach(b=>b.disabled=true);
if(isCorrect){
feedbackMsg.textContent = '';
feedbackMsg.className='feedback-msg ok';
showCartel();
} else {
feedbackMsg.textContent = `Casi... La respuesta correcta es: ${emoById(correctId).label}`;
feedbackMsg.className='feedback-msg bad';
beep('bad');
}
registerResult(isCorrect);
nextBtn.hidden = false;
}
document.getElementById('checkWriteBtn').addEventListener('click', checkAnswer);
input.addEventListener('keydown', e=>{ if(e.key==='Enter') checkAnswer(); });
input.focus();
}
/* ---------------- STATS SCREEN ---------------- */
const GAME_LABELS = {identificar:'Identificar', asociar:'Asociar', parejas:'Parejas', escribir:'Escribir'};
document.getElementById('resetStatsBtn').addEventListener('click', ()=>{
if(confirm('¿Seguro que quieres borrar todas las estadísticas?')){
stats = blankStats();
saveStats();
renderStats();
}
});
function renderStats(){
let totalIntentos=0, totalAciertos=0;
const tbody = document.getElementById('statsTableBody');
tbody.innerHTML='';
Object.keys(GAME_LABELS).forEach(g=>{
const s = stats.porJuego[g];
totalIntentos += s.intentos;
totalAciertos += s.aciertos;
const pct = s.intentos ? Math.round(100*s.aciertos/s.intentos) : 0;
const tr = document.createElement('tr');
tr.innerHTML = `<td>${GAME_LABELS[g]}</td><td>${s.intentos}</td><td>${s.aciertos}</td><td>${pct}%</td><td>${s.mejorRacha}</td>`;
tbody.appendChild(tr);
});
const pctTotal = totalIntentos ? Math.round(100*totalAciertos/totalIntentos) : 0;
document.getElementById('statsCards').innerHTML = `
<div class="stat-mini"><div class="num">${totalIntentos}</div><div class="lbl">Intentos totales</div></div>
<div class="stat-mini"><div class="num">${totalAciertos}</div><div class="lbl">Aciertos totales</div></div>
<div class="stat-mini"><div class="num">${pctTotal}%</div><div class="lbl">% de acierto</div></div>
<div class="stat-mini"><div class="num">${stats.mejorRacha}</div><div class="lbl">Mejor racha</div></div>
`;
}
/* init */
showScreen('menu');
})();
</script>
</body>
</html>