Enlaces a las aplicaciones html
https://disanedu.linexedu.eu/aplicaciones/inteligencia/dislexia/colorea_las_silabas.html (elaborado con Canvas de Gemini)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/dislexia/colorea-las-silabas.html (elaborado con Claude)
Aplicaciones html en un Site
Código html Colorea las sílabas (Gemini)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Colorea las sílabas</title>
<!-- Tailwind CSS para estilos rápidos y responsive -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome para iconos -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style id="dynamic-font-style">
/* La fuente OpenDyslexic se inyectará aquí dinámicamente para uso offline */
</style>
<script>
/*
SISTEMA DE AUTO-INSTALACIÓN OFFLINE DE FUENTE
Descarga la fuente la primera vez que abres el archivo con internet
y la "incrusta" para siempre en la memoria del navegador en formato Base64.
Las siguientes veces no necesitará conexión a internet.
*/
(async function initOfflineFont() {
const fontUrl = 'https://cdn.jsdelivr.net/npm/opendyslexic@1.0.3/fonts/OpenDyslexic-Bold.woff2';
const storageKey = 'offline_opendyslexic_font';
// 1. Comprobar si ya la tenemos guardada (offline)
const cachedFont = localStorage.getItem(storageKey);
if (cachedFont) {
// Inyectar directamente sin usar Internet
injectFont(cachedFont);
} else {
// 2. Descargarla (solo 1 vez) y convertirla a Base64
try {
const response = await fetch(fontUrl);
if (!response.ok) throw new Error("No se pudo conectar");
const blob = await response.blob();
const reader = new FileReader();
reader.onloadend = function() {
const base64data = reader.result;
localStorage.setItem(storageKey, base64data); // Guardar para siempre
injectFont(base64data);
}
reader.readAsDataURL(blob);
} catch (error) {
console.log("Modo 100% offline activo. Si la fuente no se guardó previamente, se usarán las fuentes de respaldo (Comic Sans/Andika).");
}
}
function injectFont(base64Data) {
const styleEl = document.getElementById('dynamic-font-style');
styleEl.textContent = `
@font-face {
font-family: 'OpenDyslexic';
src: url(${base64Data}) format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'OpenDyslexic';
src: url(${base64Data}) format('woff2');
font-weight: bold;
font-style: normal;
font-display: swap;
}
`;
}
})();
</script>
<style>
body {
font-family: 'OpenDyslexic', 'Comic Sans MS', 'Andika', Arial, sans-serif;
background-color: #f0fdf4; /* Verde muy suave y relajante */
color: #1f2937;
/* Prevenir scroll horizontal indeseado en móviles */
overflow-x: hidden;
overscroll-behavior-y: none; /* Evitar pull-to-refresh accidental */
}
/* Estilos específicos para la actividad */
.syllable {
transition: all 0.3s ease;
/* Borde sutil para indicar que es interactivo incluso antes de colorear */
border: 2px solid transparent;
}
.syllable:active {
transform: scale(0.95);
}
.syllable-highlight {
background-color: #fef08a; /* Amarillo suave */
border-radius: 0.5rem;
transform: scale(1.05);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.color-drop {
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.2s;
touch-action: manipulation; /* Optimiza toques en móviles */
}
.color-drop.selected {
transform: scale(1.2);
box-shadow: 0 0 0 4px white, 0 0 0 8px currentColor;
z-index: 10;
}
/* Animaciones */
@keyframes pop {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.animate-pop {
animation: pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
/* Ocultar elementos de UI basados en el estado */
.hidden { display: none !important; }
/* Estilo para asegurar que las palabras largas se ajusten */
.word-wrap-safe {
word-break: break-word;
hyphens: auto;
}
/* Ajuste de scrollbar para el gestor de palabras */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</head>
<body class="min-h-screen flex flex-col items-center justify-center relative">
<!-- BOTÓN DE SONIDO GLOBAL (Siempre visible o fácilmente accesible) -->
<button id="btn-toggle-sound" class="absolute top-2 sm:top-4 right-2 sm:right-4 w-12 h-12 sm:w-14 sm:h-14 bg-white rounded-full shadow-md flex items-center justify-center text-xl sm:text-2xl text-gray-700 hover:bg-gray-50 transition-colors z-50 focus:outline-none focus:ring-4 focus:ring-blue-300">
<i class="fas fa-volume-up" id="icon-sound"></i>
</button>
<!-- PANTALLA 1: MENÚ PRINCIPAL -->
<div id="screen-menu" class="w-full max-w-4xl mx-auto p-4 sm:p-8 flex flex-col items-center justify-center min-h-screen text-center">
<div class="bg-white p-6 sm:p-12 rounded-3xl shadow-xl w-full border-4 border-blue-100">
<h1 class="text-4xl sm:text-5xl md:text-7xl font-bold text-blue-600 mb-4 sm:mb-6 animate-float">
Colorea las sílabas
</h1>
<p class="text-lg sm:text-2xl text-gray-600 mb-8 sm:mb-12">
Escucha, separa y colorea.<br class="hidden sm:block">¡Aprender a leer es divertido!
</p>
<div class="flex flex-col gap-4 sm:gap-6 w-full max-w-md mx-auto">
<button id="btn-start" class="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-4 sm:py-5 px-6 sm:px-8 rounded-2xl text-2xl sm:text-3xl shadow-lg transition-transform transform hover:-translate-y-1 active:translate-y-0 flex items-center justify-center gap-3">
<i class="fas fa-play"></i> Comenzar
</button>
<button id="btn-manage" class="w-full bg-blue-100 hover:bg-blue-200 text-blue-700 font-bold py-3 sm:py-4 px-6 rounded-2xl text-lg sm:text-xl shadow-sm transition-colors flex items-center justify-center gap-3">
<i class="fas fa-cog"></i> Gestionar palabras
</button>
</div>
</div>
</div>
<!-- PANTALLA 2: ACTIVIDAD (JUEGO) -->
<div id="screen-game" class="hidden w-full max-w-5xl mx-auto p-2 sm:p-6 flex flex-col min-h-screen">
<!-- Barra superior: Progreso y Controles -->
<div class="flex flex-col-reverse sm:flex-row justify-between items-center gap-4 mb-4 sm:mb-8 pt-12 sm:pt-0">
<button id="btn-back-menu" class="w-full sm:w-auto bg-white hover:bg-gray-100 text-gray-700 font-bold py-2 px-4 rounded-xl shadow-sm text-base sm:text-lg transition-colors flex items-center justify-center gap-2 border-2 border-gray-200">
<i class="fas fa-arrow-left"></i> Volver
</button>
<div class="flex items-center gap-3 w-full sm:w-auto justify-center">
<span class="text-lg sm:text-xl font-bold text-gray-600" id="progress-text">Palabra 1 de 5</span>
<div class="w-32 sm:w-48 h-4 sm:h-6 bg-gray-200 rounded-full overflow-hidden shadow-inner">
<div id="progress-bar" class="h-full bg-green-500 rounded-full transition-all duration-500" style="width: 20%;"></div>
</div>
</div>
</div>
<!-- Área principal de la palabra -->
<div class="flex-grow flex flex-col items-center justify-center bg-white p-4 sm:p-10 rounded-3xl shadow-xl border-4 border-blue-50 relative min-h-[40vh] sm:min-h-[50vh]">
<!-- Contenedor dinámico (Palabra completa o Sílabas) -->
<div id="word-display-area" class="w-full flex items-center justify-center min-h-[150px] sm:min-h-[200px]">
<!-- Aquí se inyecta la palabra completa o las sílabas -->
</div>
<!-- Paleta de colores (se muestra en la Fase 3) -->
<div id="color-palette" class="hidden w-full mt-6 sm:mt-12 flex flex-wrap justify-center gap-3 sm:gap-6 p-2 sm:p-4 bg-gray-50 rounded-2xl border-2 border-gray-100">
<!-- Las gotas se generan por JS -->
</div>
</div>
<!-- Botones de Acción de la actividad -->
<div id="game-controls" class="hidden flex-col sm:flex-row justify-center gap-3 sm:gap-6 mt-4 sm:mt-8 w-full max-w-2xl mx-auto pb-6 sm:pb-0">
<button id="btn-read-syllables" class="w-full sm:flex-1 bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 sm:py-4 px-4 sm:px-6 rounded-2xl text-lg sm:text-xl shadow-lg transition-transform transform hover:-translate-y-1 active:translate-y-0 flex items-center justify-center gap-2">
<i class="fas fa-volume-up"></i> Escuchar sílabas
</button>
<button id="btn-finish-word" class="w-full sm:flex-1 bg-green-500 hover:bg-green-600 text-white font-bold py-3 sm:py-4 px-4 sm:px-6 rounded-2xl text-lg sm:text-xl shadow-lg transition-transform transform hover:-translate-y-1 active:translate-y-0 flex items-center justify-center gap-2">
<i class="fas fa-check-circle"></i> ¡Terminado!
</button>
</div>
</div>
<!-- PANTALLA 3: MENSAJE MOTIVADOR (Entre palabras) -->
<div id="screen-feedback" class="hidden fixed inset-0 bg-white/90 backdrop-blur-sm z-40 flex items-center justify-center">
<div class="text-center p-8 bg-white rounded-3xl shadow-2xl border-4 border-yellow-300 animate-pop max-w-md w-11/12">
<div id="feedback-icon" class="text-6xl sm:text-8xl mb-4 sm:mb-6">🌟</div>
<h2 id="feedback-message" class="text-3xl sm:text-5xl font-bold text-gray-800">¡Muy bien!</h2>
</div>
</div>
<!-- PANTALLA 4: PANTALLA FINAL (Sesión completada) -->
<div id="screen-end" class="hidden w-full max-w-4xl mx-auto p-4 sm:p-8 flex flex-col items-center justify-center min-h-screen text-center">
<div class="bg-white p-6 sm:p-12 rounded-3xl shadow-2xl w-full border-4 border-green-200 animate-pop">
<div class="text-7xl sm:text-9xl mb-6">🏆</div>
<h1 class="text-4xl sm:text-6xl font-bold text-green-600 mb-4">¡Reto conseguido!</h1>
<p class="text-xl sm:text-2xl text-gray-700 mb-8 sm:mb-10">
Has completado <span id="final-score" class="font-bold text-blue-600 text-3xl sm:text-4xl">5</span> palabras.<br>¡Eres un campeón/a!
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button id="btn-play-again" class="w-full sm:w-auto bg-green-500 hover:bg-green-600 text-white font-bold py-3 sm:py-4 px-6 sm:px-8 rounded-2xl text-xl sm:text-2xl shadow-lg transition-transform transform hover:-translate-y-1 active:translate-y-0 flex items-center justify-center gap-3">
<i class="fas fa-redo"></i> Volver a jugar
</button>
<button id="btn-end-menu" class="w-full sm:w-auto bg-blue-100 hover:bg-blue-200 text-blue-700 font-bold py-3 sm:py-4 px-6 sm:px-8 rounded-2xl text-xl sm:text-2xl shadow-sm transition-colors flex items-center justify-center gap-3">
<i class="fas fa-home"></i> Menú
</button>
</div>
</div>
</div>
<!-- PANTALLA 5: GESTIÓN DE PALABRAS -->
<div id="screen-manage" class="hidden w-full max-w-4xl mx-auto p-2 sm:p-6 flex flex-col min-h-screen bg-gray-50/50">
<div class="flex items-center gap-4 mb-4 sm:mb-6 pt-12 sm:pt-0">
<button id="btn-manage-back" class="bg-white hover:bg-gray-100 text-gray-700 font-bold py-2 sm:py-3 px-4 sm:px-5 rounded-xl shadow text-lg border border-gray-200">
<i class="fas fa-arrow-left"></i>
</button>
<h1 class="text-2xl sm:text-4xl font-bold text-gray-800">Gestionar Palabras</h1>
</div>
<div class="bg-white p-4 sm:p-8 rounded-3xl shadow-xl flex-grow flex flex-col border border-gray-100">
<!-- Añadir palabra -->
<div class="bg-blue-50 p-4 sm:p-6 rounded-2xl mb-6 sm:mb-8 border border-blue-100">
<h2 class="text-lg sm:text-xl font-bold text-blue-800 mb-3 sm:mb-4 flex items-center gap-2">
<i class="fas fa-plus-circle"></i> Añadir nueva palabra
</h2>
<div class="flex flex-col sm:flex-row gap-3">
<input type="text" id="input-new-word" placeholder="Ej: ma-ri-po-sa (usa guiones)" class="flex-grow p-3 sm:p-4 rounded-xl border-2 border-blue-200 focus:outline-none focus:border-blue-500 text-lg sm:text-xl font-sans">
<button id="btn-add-word" class="w-full sm:w-auto bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 sm:py-4 px-6 sm:px-8 rounded-xl transition-colors text-lg">
Añadir
</button>
</div>
<p class="text-xs sm:text-sm text-gray-500 mt-2">
<i class="fas fa-info-circle"></i> Separa las sílabas con guiones (-). Si no usas guiones, la aplicación intentará separarlas automáticamente.
</p>
<div id="add-word-error" class="hidden text-red-500 text-sm mt-2 font-bold"></div>
</div>
<!-- Lista de palabras -->
<div class="flex-grow flex flex-col">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-2">
<h2 class="text-xl font-bold text-gray-700">Banco de palabras</h2>
<button id="btn-restore-words" class="text-red-500 hover:text-red-700 text-sm font-bold flex items-center gap-1 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-lg transition-colors">
<i class="fas fa-undo"></i> Restaurar palabras iniciales
</button>
</div>
<div class="bg-gray-50 rounded-2xl border border-gray-200 flex-grow h-64 sm:h-96 overflow-y-auto p-2 sm:p-4 custom-scrollbar">
<ul id="word-list" class="space-y-2">
<!-- La lista se genera dinámicamente -->
</ul>
</div>
<p class="text-sm text-gray-500 mt-3 text-right">
Total: <span id="word-count" class="font-bold">0</span> palabras
</p>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- LÓGICA JAVASCRIPT -->
<!-- ========================================== -->
<script>
// --- 1. BANCO DE PALABRAS Y DATOS INICIALES ---
// Palabras iniciales solicitadas, ya segmentadas correctamente
const INITIAL_WORDS = [
"con-so-la", "a-pro-bar", "es-tre-cho", "lin-ter-na", "in-ten-to",
"a-tra-co", "pá-rra-fo", "fo-ro-fo", "cál-cu-lo", "di-ges-tión",
"es-tor-bar", "in-men-so", "fo-res-tal", "a-pu-ro", "con-ser-var",
"fór-mu-la", "man-za-no", "es-tre-nar", "in-te-rior", "a-pun-te",
"es-tre-llas", "in-mor-tal", "a-to-rar", "a-pren-der", "es-to-que",
"a-plas-tar", "es-ter-nón", "a-pos-tar", "fra-ga-ta", "in-mó-vil",
"a-pes-tar", "for-ma-to", "a-po-do", "pa-se-ar", "frac-tu-ra",
"pa-sa-je", "pa-sa-do", "a-po-yo", "a-pre-tar", "con-so-lar",
"es-tre-char", "pár-pa-do"
];
// Colores de alto contraste para las 8 gotas (clases de Tailwind para background y texto)
const PALETTE_COLORS = [
{ bg: 'bg-red-500', text: 'text-red-500' },
{ bg: 'bg-blue-500', text: 'text-blue-500' },
{ bg: 'bg-green-500', text: 'text-green-500' },
{ bg: 'bg-yellow-400', text: 'text-yellow-400' },
{ bg: 'bg-purple-500', text: 'text-purple-500' },
{ bg: 'bg-pink-500', text: 'text-pink-500' },
{ bg: 'bg-orange-500', text: 'text-orange-500' },
{ bg: 'bg-cyan-400', text: 'text-cyan-400' }
];
// Colores iniciales para diferenciar sílabas (antes de colorear)
const INITIAL_SYLLABLE_COLORS = [
'text-blue-600', 'text-green-600', 'text-purple-600', 'text-red-600', 'text-orange-600'
];
const FEEDBACK_MESSAGES = [
{ msg: "¡Muy bien!", icon: "🌟" },
{ msg: "¡Fantástico!", icon: "🚀" },
{ msg: "¡Lo estás haciendo genial!", icon: "🎉" },
{ msg: "¡Excelente trabajo!", icon: "🏆" },
{ msg: "¡Sigue así!", icon: "💪" },
{ msg: "¡Eres un campeón!", icon: "🥇" },
{ msg: "¡Qué bien lo has hecho!", icon: "👏" },
{ msg: "¡Cada vez lo haces mejor!", icon: "⭐" }
];
// --- 2. ESTADO DE LA APLICACIÓN ---
let state = {
words: [],
sessionWords: [],
currentWordIndex: 0,
currentWordData: null, // { full: "consola", syllables: ["con", "so", "la"] }
soundEnabled: true,
selectedColor: null,
speechSynthesis: window.speechSynthesis,
currentUtterance: null,
isPlaying: false
};
// --- 3. REFERENCIAS DOM ---
const DOM = {
screens: {
menu: document.getElementById('screen-menu'),
game: document.getElementById('screen-game'),
feedback: document.getElementById('screen-feedback'),
end: document.getElementById('screen-end'),
manage: document.getElementById('screen-manage')
},
buttons: {
toggleSound: document.getElementById('btn-toggle-sound'),
start: document.getElementById('btn-start'),
manage: document.getElementById('btn-manage'),
backMenu: document.getElementById('btn-back-menu'),
readSyllables: document.getElementById('btn-read-syllables'),
finishWord: document.getElementById('btn-finish-word'),
playAgain: document.getElementById('btn-play-again'),
endMenu: document.getElementById('btn-end-menu'),
manageBack: document.getElementById('btn-manage-back'),
addWord: document.getElementById('btn-add-word'),
restoreWords: document.getElementById('btn-restore-words')
},
game: {
wordDisplayArea: document.getElementById('word-display-area'),
colorPalette: document.getElementById('color-palette'),
controls: document.getElementById('game-controls'),
progressText: document.getElementById('progress-text'),
progressBar: document.getElementById('progressBar'),
progressBarInner: document.getElementById('progress-bar')
},
manage: {
inputNewWord: document.getElementById('input-new-word'),
wordList: document.getElementById('word-list'),
wordCount: document.getElementById('word-count'),
addError: document.getElementById('add-word-error')
},
feedback: {
icon: document.getElementById('feedback-icon'),
message: document.getElementById('feedback-message')
},
soundIcon: document.getElementById('icon-sound')
};
// --- 4. INICIALIZACIÓN ---
function init() {
loadPreferences();
loadWords();
setupEventListeners();
updateSoundUI();
}
// --- 5. GESTIÓN DE DATOS (LocalStorage) ---
function loadPreferences() {
const savedSound = localStorage.getItem('sylApp_sound');
if (savedSound !== null) {
state.soundEnabled = savedSound === 'true';
}
}
function savePreferences() {
localStorage.setItem('sylApp_sound', state.soundEnabled);
}
function loadWords() {
const savedWords = localStorage.getItem('sylApp_words');
if (savedWords) {
try {
state.words = JSON.parse(savedWords);
} catch (e) {
console.error("Error leyendo palabras guardadas. Restaurando default.");
state.words = [...INITIAL_WORDS];
}
} else {
state.words = [...INITIAL_WORDS];
}
}
function saveWords() {
localStorage.setItem('sylApp_words', JSON.stringify(state.words));
}
function restoreInitialWords() {
if (confirm("¿Estás seguro de que quieres borrar todas las palabras personalizadas y restaurar la lista original?")) {
state.words = [...INITIAL_WORDS];
saveWords();
renderWordManager();
}
}
// --- 6. UTILIDADES DE SEPARACIÓN SILÁBICA ---
function parseWord(wordString) {
// Si la palabra contiene guiones, respetamos esa separación
if (wordString.includes('-')) {
const syllables = wordString.split('-').map(s => s.trim()).filter(s => s.length > 0);
return {
full: syllables.join(''),
syllables: syllables,
original: wordString
};
}
// Si no tiene guiones, aplicamos una separación automática MUY básica.
// Para una app profesional de producción, se usaría una librería de hyphenation,
// pero para este archivo único, aplicamos reglas heurísticas simples de VC/CV
// Nota: Se avisa al usuario en la UI que es preferible usar guiones.
return {
full: wordString.trim(),
syllables: autoSyllabicate(wordString.trim()),
original: wordString.trim()
};
}
function autoSyllabicate(word) {
// Separador muy simple y rudimentario (solo como fallback si el usuario no pone guiones).
// Lo ideal es que el profesor use los guiones en la configuración.
const vocales = "aeiouáéíóúüAEIOUÁÉÍÓÚÜ";
let syllables = [];
let current = "";
for (let i = 0; i < word.length; i++) {
current += word[i];
// Regla muy simplificada: cortar después de vocal si le sigue consonante+vocal
if (vocales.includes(word[i]) && i < word.length - 2) {
if (!vocales.includes(word[i+1]) && vocales.includes(word[i+2])) {
syllables.push(current);
current = "";
} else if (i < word.length -3 && !vocales.includes(word[i+1]) && !vocales.includes(word[i+2]) && vocales.includes(word[i+3])) {
// caso cons-con-vocal (ej: abs-trac-to) - simplificado
if(['pr','pl','br','bl','cr','cl','gr','gl','fr','fl','tr','dr','ch','ll','rr'].includes(word.substring(i+1,i+3).toLowerCase())){
syllables.push(current);
current = "";
} else {
current += word[i+1];
syllables.push(current);
current = "";
i++;
}
}
}
}
if (current) syllables.push(current);
// Si falla y devuelve un array vacío, devolvemos la palabra entera
return syllables.length > 0 ? syllables : [word];
}
// --- 7. NAVEGACIÓN Y FLUJO DE PANTALLAS ---
function showScreen(screenName) {
Object.values(DOM.screens).forEach(screen => screen.classList.add('hidden'));
DOM.screens[screenName].classList.remove('hidden');
stopAudio(); // Detener audios al cambiar de pantalla
}
// --- 8. LÓGICA DEL JUEGO (ACTIVIDAD) ---
function startSession() {
// Seleccionar 5 palabras aleatorias
if (state.words.length < 5) {
alert("Se necesitan al menos 5 palabras en el banco para jugar. Ve a gestionar palabras.");
return;
}
let shuffled = [...state.words].sort(() => 0.5 - Math.random());
state.sessionWords = shuffled.slice(0, 5).map(w => parseWord(w));
state.currentWordIndex = 0;
showScreen('game');
loadCurrentWord();
}
function updateProgress() {
const current = state.currentWordIndex + 1;
const total = state.sessionWords.length;
DOM.game.progressText.textContent = `Palabra ${current} de ${total}`;
DOM.game.progressBarInner.style.width = `${(current / total) * 100}%`;
}
function loadCurrentWord() {
updateProgress();
state.currentWordData = state.sessionWords[state.currentWordIndex];
state.selectedColor = null;
// Ocultar controles y paleta al inicio
DOM.game.controls.classList.add('hidden');
DOM.game.colorPalette.classList.add('hidden');
// Fase 1: Mostrar palabra completa
renderFullWord();
// Reproducir palabra entera
speak(state.currentWordData.full, () => {
// Fase 2: Mostrar dividida en sílabas (después de una breve pausa o si el sonido está apagado)
setTimeout(() => {
renderInitialSyllables();
// Fase 3: Transición a coloreado (después de 3 segundos)
setTimeout(() => {
prepareForColoring();
}, 3000);
}, 1000);
});
}
function renderFullWord() {
DOM.game.wordDisplayArea.innerHTML = '';
const wordEl = document.createElement('div');
// Clases responsivas: Menor tamaño en móvil, grande en desktop
wordEl.className = 'text-5xl sm:text-7xl md:text-8xl lg:text-9xl font-bold mb-4 sm:mb-8 text-center text-gray-800 word-wrap-safe w-full px-2 animate-pop';
wordEl.textContent = state.currentWordData.full;
DOM.game.wordDisplayArea.appendChild(wordEl);
}
function renderInitialSyllables() {
DOM.game.wordDisplayArea.innerHTML = '';
const container = document.createElement('div');
// flex-wrap asegura que las sílabas caigan a la siguiente línea en móviles si es muy larga
container.className = 'flex flex-wrap items-center justify-center gap-1 sm:gap-2 animate-pop w-full';
state.currentWordData.syllables.forEach((syllable, index) => {
const sylEl = document.createElement('span');
// Tallas ajustadas para móvil
sylEl.className = `text-4xl sm:text-6xl md:text-8xl font-bold px-1 sm:px-2 transition-colors duration-500 ${INITIAL_SYLLABLE_COLORS[index % INITIAL_SYLLABLE_COLORS.length]}`;
sylEl.textContent = syllable;
container.appendChild(sylEl);
// Añadir guion
if (index < state.currentWordData.syllables.length - 1) {
const dashEl = document.createElement('span');
dashEl.className = 'text-3xl sm:text-5xl md:text-7xl font-bold text-gray-400';
dashEl.textContent = '-';
container.appendChild(dashEl);
}
});
DOM.game.wordDisplayArea.appendChild(container);
}
function prepareForColoring() {
// Cambiar todas las sílabas a negro e hacerlas interactivas
const container = DOM.game.wordDisplayArea.querySelector('div');
if(!container) return; // Por si el usuario salió rápido
container.innerHTML = ''; // Reconstruir para añadir eventos
state.currentWordData.syllables.forEach((syllable, index) => {
const sylContainer = document.createElement('div');
sylContainer.className = 'relative inline-block mx-1';
const sylEl = document.createElement('span');
// Padding mayor en móvil para facilitar el toque (touch target)
sylEl.className = 'syllable inline-block text-4xl sm:text-5xl md:text-7xl font-bold cursor-pointer transition-colors duration-200 p-2 sm:p-3 md:p-4 rounded-xl select-none text-gray-800 bg-transparent';
sylEl.textContent = syllable;
sylEl.dataset.index = index;
sylEl.dataset.originalText = syllable; // Guardar para TextToSpeech
// Evento de coloreado
sylEl.addEventListener('click', () => {
if (state.selectedColor) {
// Remover colores de texto previos de Tailwind
PALETTE_COLORS.forEach(c => sylEl.classList.remove(c.text));
// Añadir el nuevo
sylEl.classList.add(state.selectedColor.text);
// Efecto visual al pintar
sylEl.classList.add('scale-110');
setTimeout(() => sylEl.classList.remove('scale-110'), 200);
}
});
container.appendChild(sylEl);
if (index < state.currentWordData.syllables.length - 1) {
const dashEl = document.createElement('span');
dashEl.className = 'text-4xl sm:text-5xl md:text-7xl font-bold text-gray-300 select-none';
dashEl.textContent = '-';
container.appendChild(dashEl);
}
});
// Generar paleta aleatoria
renderPalette();
// Mostrar UI
DOM.game.colorPalette.classList.remove('hidden');
DOM.game.colorPalette.classList.add('flex', 'animate-pop');
DOM.game.controls.classList.remove('hidden');
DOM.game.controls.classList.add('flex', 'animate-pop');
}
function renderPalette() {
DOM.game.colorPalette.innerHTML = '';
// Desordenar colores para que no estén en la misma posición siempre
const shuffledColors = [...PALETTE_COLORS].sort(() => 0.5 - Math.random());
shuffledColors.forEach(colorObj => {
const drop = document.createElement('button');
// w-12 h-12 = 48x48px (ideal para toque en móvil)
drop.className = `color-drop w-12 h-12 sm:w-14 md:w-16 sm:h-14 md:h-16 rounded-full shadow-md flex-shrink-0 focus:outline-none ${colorObj.bg}`;
drop.dataset.colorClass = colorObj.text;
drop.addEventListener('click', () => {
// Quitar selección previa
document.querySelectorAll('.color-drop').forEach(d => d.classList.remove('selected'));
// Seleccionar actual
drop.classList.add('selected');
state.selectedColor = colorObj;
});
DOM.game.colorPalette.appendChild(drop);
});
// Seleccionar el primero por defecto (opcional, pero ayuda)
// state.selectedColor = null; // Decidimos obligar al niño a tocar un color primero
}
// --- 9. REPRODUCCIÓN DE AUDIO (Web Speech API) ---
function updateSoundUI() {
if (state.soundEnabled) {
DOM.soundIcon.classList.remove('fa-volume-mute', 'text-red-500');
DOM.soundIcon.classList.add('fa-volume-up', 'text-gray-700');
} else {
DOM.soundIcon.classList.remove('fa-volume-up', 'text-gray-700');
DOM.soundIcon.classList.add('fa-volume-mute', 'text-red-500');
}
}
function toggleSound() {
state.soundEnabled = !state.soundEnabled;
savePreferences();
updateSoundUI();
if (!state.soundEnabled) {
stopAudio();
}
}
function stopAudio() {
if (state.speechSynthesis) {
state.speechSynthesis.cancel();
}
state.isPlaying = false;
// Limpiar resaltados visuales si los hay
document.querySelectorAll('.syllable-highlight').forEach(el => {
el.classList.remove('syllable-highlight');
});
// Limpiar también las nuevas clases de resaltado intensivo
document.querySelectorAll('.syllable').forEach(el => {
el.classList.remove('ring-4', 'ring-yellow-400', 'bg-yellow-100', 'scale-110', 'z-10');
});
}
function getSpanishVoice() {
const voices = state.speechSynthesis.getVoices();
// Buscar español de España primero
let voice = voices.find(v => v.lang === 'es-ES' || v.lang === 'es_ES');
// Si no, cualquier español
if (!voice) {
voice = voices.find(v => v.lang.startsWith('es'));
}
return voice;
}
function speak(text, onEndCallback = null, rate = 0.8) {
stopAudio();
if (!state.soundEnabled) {
if (onEndCallback) onEndCallback();
return;
}
state.isPlaying = true;
state.currentUtterance = new SpeechSynthesisUtterance(text);
state.currentUtterance.lang = 'es-ES';
state.currentUtterance.rate = rate; // Hablar un poco más despacio
state.currentUtterance.pitch = 1.1; // Tono ligeramente más agudo/amigable
const voice = getSpanishVoice();
if (voice) {
state.currentUtterance.voice = voice;
}
state.currentUtterance.onend = () => {
state.isPlaying = false;
if (onEndCallback) onEndCallback();
};
state.currentUtterance.onerror = (e) => {
console.error("Speech error:", e);
state.isPlaying = false;
if (onEndCallback) onEndCallback(); // Continuar flujo incluso si hay error
};
state.speechSynthesis.speak(state.currentUtterance);
}
function playSyllableBySyllable() {
if (state.isPlaying) {
stopAudio();
return; // Si ya estaba sonando, lo detenemos y salimos (actúa como toggle)
}
const syllablesDOM = Array.from(document.querySelectorAll('.syllable'));
if(syllablesDOM.length === 0) return;
let index = 0;
function playNext() {
if (index < syllablesDOM.length) {
const sylEl = syllablesDOM[index];
const text = sylEl.dataset.originalText;
// Llamar a speak() PRIMERO (ya que internamente llama a stopAudio y borra estilos)
speak(text, () => {
// Al terminar de hablar esta sílaba, quitamos el resaltado
sylEl.classList.remove('syllable-highlight', 'ring-4', 'ring-yellow-400', 'bg-yellow-100', 'scale-110', 'z-10');
index++;
setTimeout(playNext, 400); // Pausa entre sílabas
}, 0.7); // Aún más despacio para sílabas
// Aplicar el resaltado intenso DESPUÉS de llamar a speak()
syllablesDOM.forEach(el => el.classList.remove('syllable-highlight', 'ring-4', 'ring-yellow-400', 'bg-yellow-100', 'scale-110', 'z-10'));
sylEl.classList.add('syllable-highlight', 'ring-4', 'ring-yellow-400', 'bg-yellow-100', 'scale-110', 'z-10');
} else {
// Al terminar las sílabas, leer palabra completa
setTimeout(() => {
// Efecto visual sutil y evidente a toda la palabra
const container = DOM.game.wordDisplayArea.querySelector('div');
if(container) {
container.classList.add('ring-4', 'ring-blue-400', 'bg-blue-50', 'rounded-3xl', 'p-4', 'transition-all', 'scale-105');
}
speak(state.currentWordData.full, () => {
if(container) {
container.classList.remove('ring-4', 'ring-blue-400', 'bg-blue-50', 'rounded-3xl', 'p-4', 'scale-105');
}
});
}, 500);
}
}
playNext();
}
// --- 10. FLUJO DE FIN DE PALABRA Y SESIÓN ---
function finishWord() {
stopAudio();
// Mostrar feedback
const feedback = FEEDBACK_MESSAGES[Math.floor(Math.random() * FEEDBACK_MESSAGES.length)];
DOM.feedback.icon.textContent = feedback.icon;
DOM.feedback.message.textContent = feedback.msg;
DOM.screens.feedback.classList.remove('hidden');
// Sonido de éxito corto (si está activado)
if(state.soundEnabled) {
speak(feedback.msg, null, 1.0);
}
setTimeout(() => {
DOM.screens.feedback.classList.add('hidden');
// Siguiente palabra o fin
state.currentWordIndex++;
if (state.currentWordIndex < state.sessionWords.length) {
loadCurrentWord();
} else {
showEndScreen();
}
}, 2500);
}
function showEndScreen() {
showScreen('end');
if(state.soundEnabled) {
speak("¡Reto conseguido! Eres un campeón.");
}
}
function handleBackToMenu() {
if (state.currentWordIndex > 0 && state.currentWordIndex < state.sessionWords.length) {
if (confirm("¿Estás seguro de que quieres salir? Perderás el progreso de esta sesión.")) {
showScreen('menu');
}
} else {
showScreen('menu');
}
}
// --- 11. GESTIÓN DE PALABRAS (UI) ---
function renderWordManager() {
DOM.manage.wordList.innerHTML = '';
// Ordenar alfabéticamente para visualización
const sortedWords = [...state.words].sort((a,b) => a.localeCompare(b));
sortedWords.forEach(wordStr => {
const li = document.createElement('li');
li.className = 'flex justify-between items-center p-3 sm:p-4 bg-white rounded-xl border border-gray-100 shadow-sm hover:shadow-md transition-shadow';
const wordSpan = document.createElement('span');
wordSpan.className = 'text-lg sm:text-xl font-medium text-gray-800 break-all mr-2';
wordSpan.textContent = wordStr;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'text-red-500 hover:text-red-700 hover:bg-red-50 p-2 sm:p-3 rounded-lg transition-colors flex-shrink-0';
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
deleteBtn.title = "Eliminar palabra";
deleteBtn.onclick = () => deleteWord(wordStr);
li.appendChild(wordSpan);
li.appendChild(deleteBtn);
DOM.manage.wordList.appendChild(li);
});
DOM.manage.wordCount.textContent = state.words.length;
}
function addWord() {
const newWord = DOM.manage.inputNewWord.value.trim().toLowerCase();
const errorEl = DOM.manage.addError;
errorEl.classList.add('hidden');
if (!newWord) return;
// Validación básica: solo letras, guiones y espacios (para palabras compuestas)
if (!/^[a-záéíóúüñ\-\s]+$/.test(newWord)) {
errorEl.textContent = "La palabra contiene caracteres no válidos. Usa solo letras y guiones.";
errorEl.classList.remove('hidden');
return;
}
// Evitar duplicados
if (state.words.includes(newWord)) {
errorEl.textContent = "Esta palabra ya existe en el banco.";
errorEl.classList.remove('hidden');
return;
}
state.words.push(newWord);
saveWords();
renderWordManager();
DOM.manage.inputNewWord.value = ''; // Limpiar input
}
function deleteWord(wordToDelete) {
if (confirm(`¿Eliminar la palabra "${wordToDelete}"?`)) {
state.words = state.words.filter(w => w !== wordToDelete);
saveWords();
renderWordManager();
}
}
// --- 12. EVENT LISTENERS ---
function setupEventListeners() {
// Sonido
DOM.buttons.toggleSound.addEventListener('click', toggleSound);
// Menú
DOM.buttons.start.addEventListener('click', startSession);
DOM.buttons.manage.addEventListener('click', () => {
renderWordManager();
showScreen('manage');
});
// Actividad
DOM.buttons.backMenu.addEventListener('click', handleBackToMenu);
DOM.buttons.readSyllables.addEventListener('click', playSyllableBySyllable);
DOM.buttons.finishWord.addEventListener('click', finishWord);
// Fin
DOM.buttons.playAgain.addEventListener('click', startSession);
DOM.buttons.endMenu.addEventListener('click', () => showScreen('menu'));
// Gestión
DOM.buttons.manageBack.addEventListener('click', () => showScreen('menu'));
DOM.buttons.addWord.addEventListener('click', addWord);
DOM.manage.inputNewWord.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addWord();
});
DOM.buttons.restoreWords.addEventListener('click', restoreInitialWords);
// Cargar voces si el navegador las carga asíncronamente
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = () => {
// Voces cargadas, listas para usar
};
}
}
// --- ARRANQUE ---
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
Código html Colorea las sílabas (Claude)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Colorea las sílabas</title>
<!-- Fuente OpenDyslexic (con alternativa local si no hay conexión) -->
<style>
@font-face {
font-family: 'OpenDyslexic';
src: url('https://cdn.jsdelivr.net/fontsource/fonts/opendyslexic@5.3.0/latin-400-normal.woff2') format('woff2'),
url('https://cdn.jsdelivr.net/fontsource/fonts/opendyslexic@5.3.0/latin-400-normal.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('https://cdn.jsdelivr.net/fontsource/fonts/opendyslexic@5.3.0/latin-700-normal.woff2') format('woff2'),
url('https://cdn.jsdelivr.net/fontsource/fonts/opendyslexic@5.3.0/latin-700-normal.woff') format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
:root{
--color-fondo: #f4f7fb;
--color-tarjeta: #ffffff;
--color-texto: #1f2937;
--color-texto-suave: #4b5563;
--color-primario: #2563eb;
--color-primario-oscuro: #1d4ed8;
--color-exito: #16a34a;
--color-peligro: #dc2626;
--color-borde: #d7dee8;
--radio: 18px;
--sombra: 0 6px 20px rgba(30,41,59,0.10);
--fuente-principal: 'OpenDyslexic', 'Comic Sans MS', 'Verdana', sans-serif;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
min-height: 100%;
background: linear-gradient(180deg, #eef3fb 0%, #f7f9fc 100%);
font-family: var(--fuente-principal);
color: var(--color-texto);
}
body {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 16px;
}
.oculto { display: none !important; }
.contenedor-app {
width: 100%;
max-width: 900px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* ---------- Cabecera común ---------- */
.barra-superior {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.titulo-app {
font-size: clamp(1.4rem, 3vw, 2.1rem);
margin: 0;
color: var(--color-primario-oscuro);
}
button {
font-family: inherit;
cursor: pointer;
border-radius: 14px;
border: 2px solid transparent;
font-size: 1rem;
transition: transform 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease;
}
button:focus-visible, [tabindex]:focus-visible, .gota:focus-visible {
outline: 4px solid #f59e0b;
outline-offset: 3px;
}
button:active { transform: scale(0.97); }
.boton-primario {
background: var(--color-primario);
color: #fff;
padding: 14px 26px;
font-weight: bold;
box-shadow: var(--sombra);
}
.boton-primario:hover { background: var(--color-primario-oscuro); }
.boton-secundario {
background: #fff;
color: var(--color-primario-oscuro);
border: 2px solid var(--color-primario);
padding: 12px 22px;
font-weight: bold;
}
.boton-secundario:hover { background: #eaf1ff; }
.boton-sonido {
background: #fff;
border: 2px solid var(--color-borde);
border-radius: 50%;
width: 54px;
height: 54px;
font-size: 1.4rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--sombra);
}
.boton-peligro {
background: #fff;
color: var(--color-peligro);
border: 2px solid var(--color-peligro);
padding: 10px 18px;
font-weight: bold;
}
.boton-peligro:hover { background: #fdeaea; }
/* ---------- Tarjetas ---------- */
.tarjeta {
background: var(--color-tarjeta);
border-radius: var(--radio);
box-shadow: var(--sombra);
padding: clamp(18px, 4vw, 40px);
}
/* ---------- Pantalla Menú ---------- */
#pantalla-menu .tarjeta {
text-align: center;
}
#pantalla-menu p.explicacion {
font-size: 1.15rem;
color: var(--color-texto-suave);
max-width: 620px;
margin: 12px auto 28px;
line-height: 1.6;
}
.icono-grande {
font-size: 3.4rem;
margin-bottom: 6px;
}
.fila-botones-menu {
display: flex;
justify-content: center;
gap: 18px;
flex-wrap: wrap;
margin-top: 10px;
}
/* ---------- Progreso ---------- */
.progreso-info {
display: flex;
align-items: center;
gap: 14px;
font-weight: bold;
color: var(--color-texto-suave);
}
.barra-progreso-exterior {
flex: 1;
height: 14px;
background: #e2e8f0;
border-radius: 999px;
overflow: hidden;
min-width: 120px;
}
.barra-progreso-interior {
height: 100%;
background: linear-gradient(90deg, #2563eb, #16a34a);
border-radius: 999px;
transition: width 0.4s ease;
}
/* ---------- Zona de la palabra ---------- */
.zona-palabra {
text-align: center;
padding: 30px 10px;
}
.palabra-completa {
font-size: clamp(2.2rem, 7vw, 4rem);
font-weight: bold;
letter-spacing: 1px;
word-break: break-word;
}
.silabas-contenedor {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: clamp(2.2rem, 7vw, 4rem);
font-weight: bold;
}
.silaba {
padding: 6px 10px;
border-radius: 12px;
transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
min-width: 1ch;
}
.silaba.coloreable {
cursor: pointer;
background-color: #e5e7eb;
color: #111827;
}
.silaba.coloreable:hover { transform: translateY(-2px); }
.silaba.resaltada {
box-shadow: 0 0 0 5px #f59e0b;
transform: scale(1.08);
}
.guion-silaba {
color: #9ca3af;
font-weight: normal;
}
.efecto-audio-palabra {
display: inline-flex;
gap: 5px;
align-items: flex-end;
height: 26px;
margin-top: 14px;
}
.efecto-audio-palabra span {
display: block;
width: 6px;
background: var(--color-primario);
border-radius: 3px;
animation: onda 0.9s ease-in-out infinite;
}
.efecto-audio-palabra span:nth-child(1){ height: 10px; animation-delay: 0s;}
.efecto-audio-palabra span:nth-child(2){ height: 22px; animation-delay: 0.15s;}
.efecto-audio-palabra span:nth-child(3){ height: 14px; animation-delay: 0.3s;}
.efecto-audio-palabra span:nth-child(4){ height: 20px; animation-delay: 0.45s;}
@keyframes onda {
0%, 100% { transform: scaleY(0.5); }
50% { transform: scaleY(1); }
}
/* ---------- Paleta de gotas ---------- */
.paleta-gotas {
display: flex;
justify-content: center;
gap: 14px;
flex-wrap: wrap;
margin-top: 26px;
padding-top: 20px;
border-top: 2px dashed var(--color-borde);
}
.gota {
width: 58px;
height: 58px;
border-radius: 50% 50% 50% 0;
transform: rotate(45deg);
border: 3px solid rgba(0,0,0,0.15);
cursor: pointer;
position: relative;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.gota:hover { transform: rotate(45deg) scale(1.08); }
.gota.seleccionada {
box-shadow: 0 0 0 5px #111827, 0 0 0 9px #fff;
}
.gota .marca-seleccion {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(-45deg);
color: #fff;
font-size: 1.3rem;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0,0,0,0.4);
}
/* ---------- Controles de actividad ---------- */
.fila-controles-actividad {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
margin-top: 26px;
}
/* ---------- Refuerzo motivador ---------- */
#pantalla-refuerzo .tarjeta {
text-align: center;
}
.mensaje-motivador {
font-size: clamp(1.8rem, 5vw, 2.6rem);
color: var(--color-exito);
font-weight: bold;
margin: 10px 0 20px;
}
.emoji-refuerzo {
font-size: 4rem;
}
/* ---------- Pantalla final ---------- */
#pantalla-final .tarjeta { text-align: center; }
.resumen-final {
font-size: 1.3rem;
color: var(--color-texto-suave);
margin: 14px 0 26px;
}
/* ---------- Confeti ---------- */
.capa-confeti {
position: fixed;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 50;
}
.trozo-confeti {
position: absolute;
top: -20px;
width: 12px;
height: 12px;
border-radius: 3px;
animation-name: caer;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
@keyframes caer {
to { transform: translateY(110vh) rotate(360deg); opacity: 0.3; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
/* ---------- Gestión de palabras ---------- */
.lista-palabras {
list-style: none;
margin: 20px 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
max-height: 420px;
overflow-y: auto;
}
.fila-palabra {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 16px;
border: 2px solid var(--color-borde);
border-radius: 12px;
flex-wrap: wrap;
}
.fila-palabra .texto-palabra {
font-size: 1.15rem;
font-weight: bold;
}
.etiqueta-origen {
font-size: 0.75rem;
font-weight: normal;
color: var(--color-texto-suave);
background: #eef2ff;
padding: 2px 8px;
border-radius: 999px;
margin-left: 8px;
}
.fila-palabra .acciones-palabra {
display: flex;
gap: 8px;
}
.fila-palabra .acciones-palabra button {
padding: 8px 14px;
font-size: 0.9rem;
}
.formulario-palabra {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 18px;
padding-top: 18px;
border-top: 2px dashed var(--color-borde);
}
.formulario-palabra label {
font-weight: bold;
}
.formulario-palabra input[type="text"] {
font-family: inherit;
font-size: 1.1rem;
padding: 12px 14px;
border-radius: 12px;
border: 2px solid var(--color-borde);
width: 100%;
}
.formulario-palabra input[type="text"]:focus-visible {
outline: 4px solid #f59e0b;
border-color: #f59e0b;
}
.ayuda-formulario {
font-size: 0.9rem;
color: var(--color-texto-suave);
}
.fila-botones-formulario {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.aviso-guardado {
font-weight: bold;
color: var(--color-exito);
min-height: 1.4em;
}
/* ---------- Cuadro de confirmación (modal accesible) ---------- */
.fondo-modal {
position: fixed;
inset: 0;
background: rgba(15,23,42,0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 16px;
}
.caja-modal {
background: #fff;
border-radius: var(--radio);
padding: 28px;
max-width: 420px;
width: 100%;
box-shadow: var(--sombra);
text-align: center;
}
.caja-modal p { font-size: 1.15rem; margin-bottom: 22px; }
.caja-modal .fila-botones-formulario { justify-content: center; }
.pie-app {
text-align: center;
color: #9aa5b1;
font-size: 0.8rem;
padding: 10px 0 4px;
}
.solo-lectura-oculto:not(:focus) {
position: absolute !important;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
</style>
</head>
<body>
<div class="contenedor-app" id="app">
<!-- Región para anuncios a lectores de pantalla -->
<div id="anunciador" class="solo-lectura-oculto" aria-live="polite"></div>
<!-- ======================= PANTALLA MENÚ ======================= -->
<main id="pantalla-menu">
<div class="barra-superior">
<h1 class="titulo-app">🎨 Colorea las sílabas</h1>
<button id="boton-sonido-menu" class="boton-sonido" type="button" aria-pressed="true">🔊</button>
</div>
<div class="tarjeta">
<div class="icono-grande" aria-hidden="true">🖍️</div>
<p class="explicacion">
Escucha la palabra, obsérvala dividida en sílabas y coloréalas con los colores que más te gusten.
¡No hay respuestas incorrectas, solo diversión aprendiendo!
</p>
<div class="fila-botones-menu">
<button id="boton-comenzar" class="boton-primario" type="button">▶️ Comenzar</button>
<button id="boton-gestionar" class="boton-secundario" type="button">⚙️ Gestionar palabras</button>
</div>
</div>
<p class="pie-app">Aplicación educativa para el trabajo de la conciencia silábica</p>
</main>
<!-- ======================= PANTALLA DE JUEGO ======================= -->
<main id="pantalla-juego" class="oculto">
<div class="barra-superior">
<div class="progreso-info">
<span id="texto-progreso">Palabra 1 de 5</span>
<div class="barra-progreso-exterior">
<div id="barra-progreso-interior" class="barra-progreso-interior" style="width:20%;"></div>
</div>
</div>
<div style="display:flex; gap:10px;">
<button id="boton-sonido-juego" class="boton-sonido" type="button" aria-pressed="true">🔊</button>
<button id="boton-menu-desde-juego" class="boton-secundario" type="button">🏠 Volver al menú</button>
</div>
</div>
<div class="tarjeta zona-palabra">
<div id="vista-palabra-completa">
<p class="palabra-completa" id="texto-palabra-completa">palabra</p>
<div class="efecto-audio-palabra oculto" id="efecto-audio-palabra" aria-hidden="true">
<span></span><span></span><span></span><span></span>
</div>
</div>
<div id="vista-silabas" class="silabas-contenedor" role="group" aria-label="Sílabas de la palabra"></div>
<div id="zona-coloreado" class="oculto">
<div class="paleta-gotas" id="paleta-gotas" role="group" aria-label="Paleta de colores"></div>
<div class="fila-controles-actividad">
<button id="boton-escuchar-silabas" class="boton-secundario" type="button">🔊 Escuchar sílaba a sílaba</button>
<button id="boton-palabra-terminada" class="boton-primario" type="button">✅ Palabra terminada</button>
</div>
</div>
</div>
</main>
<!-- ======================= PANTALLA REFUERZO ======================= -->
<main id="pantalla-refuerzo" class="oculto">
<div class="tarjeta">
<div class="emoji-refuerzo" id="emoji-refuerzo" aria-hidden="true">🎉</div>
<p class="mensaje-motivador" id="texto-mensaje-motivador">¡Muy bien!</p>
</div>
</main>
<!-- ======================= PANTALLA FINAL ======================= -->
<main id="pantalla-final" class="oculto">
<div class="tarjeta">
<div class="icono-grande" aria-hidden="true">🏆</div>
<h2>¡Enhorabuena!</h2>
<p class="resumen-final" id="texto-resumen-final">Has completado 5 de 5 palabras.</p>
<div class="fila-botones-menu">
<button id="boton-volver-a-jugar" class="boton-primario" type="button">🔄 Volver a jugar</button>
<button id="boton-ir-a-menu-final" class="boton-secundario" type="button">🏠 Menú principal</button>
</div>
</div>
</main>
<!-- ======================= GESTIÓN DE PALABRAS ======================= -->
<main id="pantalla-gestion" class="oculto">
<div class="barra-superior">
<h1 class="titulo-app">⚙️ Gestionar palabras</h1>
<button id="boton-volver-menu-gestion" class="boton-secundario" type="button">🏠 Volver al menú</button>
</div>
<div class="tarjeta">
<p>Banco de palabras disponible (<span id="contador-palabras">0</span> en total).</p>
<ul class="lista-palabras" id="lista-palabras" aria-label="Lista de palabras"></ul>
<div class="formulario-palabra">
<label for="campo-silabas">Añadir o editar palabra (separa las sílabas con guiones)</label>
<input type="text" id="campo-silabas" placeholder="Ejemplo: ma-ri-po-sa" autocomplete="off">
<p class="ayuda-formulario">Escribe la palabra dividiendo cada sílaba con un guion «-». Ejemplo: ca-mi-se-ta</p>
<div class="fila-botones-formulario">
<button id="boton-guardar-palabra" class="boton-primario" type="button">💾 Guardar palabra</button>
<button id="boton-cancelar-edicion" class="boton-secundario oculto" type="button">✖️ Cancelar edición</button>
<button id="boton-restaurar-banco" class="boton-peligro" type="button">♻️ Restaurar banco inicial</button>
</div>
<p class="aviso-guardado" id="aviso-guardado" role="status"></p>
</div>
</div>
</main>
<!-- ======================= MODAL DE CONFIRMACIÓN ======================= -->
<div id="fondo-modal" class="fondo-modal oculto" role="dialog" aria-modal="true" aria-labelledby="texto-modal">
<div class="caja-modal">
<p id="texto-modal">¿Estás seguro?</p>
<div class="fila-botones-formulario">
<button id="boton-modal-confirmar" class="boton-peligro" type="button">Sí, continuar</button>
<button id="boton-modal-cancelar" class="boton-secundario" type="button">Cancelar</button>
</div>
</div>
</div>
</div>
<script>
(function () {
"use strict";
/* =====================================================================
1. BANCO DE PALABRAS INICIAL
Cada palabra se almacena ya segmentada en sílabas (comprobadas
manualmente conforme a las reglas de silabeo del español) para
garantizar una segmentación fiable.
===================================================================== */
const BANCO_INICIAL = [
["con","so","la"], // consola
["a","pro","bar"], // aprobar
["es","tre","cho"], // estrecho
["lin","ter","na"], // linterna
["in","ten","to"], // intento
["a","tra","co"], // atraco
["pá","rra","fo"], // párrafo
["fo","ro","fo"], // forofo
["cál","cu","lo"], // cálculo
["di","ges","tión"], // digestión
["es","tor","bar"], // estorbar
["in","men","so"], // inmenso
["fo","res","tal"], // forestal
["a","pu","ro"], // apuro
["con","ser","var"], // conservar
["fór","mu","la"], // fórmula
["man","za","no"], // manzano
["es","tre","nar"], // estrenar
["in","te","rior"], // interior
["a","pun","te"], // apunte
["es","tre","llas"], // estrellas
["in","mor","tal"], // inmortal
["a","to","rar"], // atorar
["a","pren","der"], // aprender
["es","to","que"], // estoque
["a","plas","tar"], // aplastar
["es","ter","nón"], // esternón
["a","pos","tar"], // apostar
["fra","ga","ta"], // fragata
["in","mó","vil"], // inmóvil
["a","pes","tar"], // apestar
["for","ma","to"], // formato
["a","po","do"], // apodo
["pa","se","ar"], // pasear
["frac","tu","ra"], // fractura
["pa","sa","je"], // pasaje
["pa","sa","do"], // pasado
["a","po","yo"], // apoyo
["in","te","rés"], // interés
["a","pre","tar"], // apretar
["con","so","lar"], // consolar
["es","tre","char"], // estrechar
["pár","pa","do"] // párpado
].map(silabas => ({ silabas, origen: "inicial" }));
const CLAVE_PALABRAS = "coloreaSilabas_palabras";
const CLAVE_SONIDO = "coloreaSilabas_sonidoActivo";
const COLORES_GOTAS = [
{ nombre: "rojo", valor: "#e53935" },
{ nombre: "naranja", valor: "#fb8c00" },
{ nombre: "amarillo", valor: "#fdd835" },
{ nombre: "verde", valor: "#43a047" },
{ nombre: "azul", valor: "#1e88e5" },
{ nombre: "morado", valor: "#8e24aa" },
{ nombre: "rosa", valor: "#ec407a" },
{ nombre: "marrón", valor: "#6d4c41" }
];
const COLORES_SILABAS_INICIALES = ["#1e88e5", "#e53935", "#43a047", "#fb8c00"];
const MENSAJES_MOTIVADORES = [
"¡Muy bien!", "¡Fantástico!", "¡Lo estás haciendo genial!",
"¡Excelente trabajo!", "¡Sigue así!", "¡Eres un campeón!",
"¡Qué bien lo has hecho!", "¡Cada vez lo haces mejor!"
];
const EMOJIS_REFUERZO = ["🎉","⭐","🏆","😄","🎊","🥳"];
const PALABRAS_POR_SESION = 5;
const TIEMPO_ANTES_DE_DIVIDIR_MS = 2600; // tiempo que la palabra completa permanece visible antes de dividirse en sílabas
const TIEMPO_TRANSICION_COLOREADO_MS = 3000;
const TIEMPO_ANTES_SIGUIENTE_PALABRA_MS = 2200;
const PAUSA_ENTRE_SILABAS_MS = 550;
const prefiereMenosMovimiento = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* =====================================================================
2. ESTADO DE LA APLICACIÓN
===================================================================== */
const estado = {
palabras: [], // banco completo (inicial + personalizadas)
sonidoActivo: true,
sesion: {
palabrasSesion: [], // 5 palabras elegidas para esta partida
indiceActual: 0,
colorSeleccionado: null,
utteranceActual: null,
reproduciendoSecuencia: false
},
edicion: {
activo: false,
indiceOriginal: null
},
accionModalConfirmar: null
};
/* =====================================================================
3. UTILIDADES GENERALES
===================================================================== */
function $(id) { return document.getElementById(id); }
function anunciar(texto) {
const region = $("anunciador");
region.textContent = "";
window.setTimeout(() => { region.textContent = texto; }, 50);
}
function normalizarPalabra(silabas) {
return silabas.join("").toLowerCase();
}
function mezclarArray(array) {
const copia = array.slice();
for (let i = copia.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copia[i], copia[j]] = [copia[j], copia[i]];
}
return copia;
}
function mostrarPantalla(idPantalla) {
["pantalla-menu","pantalla-juego","pantalla-refuerzo","pantalla-final","pantalla-gestion"]
.forEach(id => $(id).classList.toggle("oculto", id !== idPantalla));
}
/* =====================================================================
4. PERSISTENCIA (localStorage)
===================================================================== */
function cargarPalabras() {
try {
const guardadas = JSON.parse(localStorage.getItem(CLAVE_PALABRAS));
if (Array.isArray(guardadas) && guardadas.length > 0) {
estado.palabras = guardadas;
return;
}
} catch (error) {
console.warn("No se pudieron leer las palabras guardadas, se usará el banco inicial.", error);
}
estado.palabras = BANCO_INICIAL.map(p => ({ silabas: p.silabas.slice(), origen: p.origen }));
guardarPalabras();
}
function guardarPalabras() {
try {
localStorage.setItem(CLAVE_PALABRAS, JSON.stringify(estado.palabras));
} catch (error) {
console.warn("No se pudo guardar el banco de palabras.", error);
}
}
function cargarPreferenciaSonido() {
const guardado = localStorage.getItem(CLAVE_SONIDO);
estado.sonidoActivo = guardado === null ? true : guardado === "true";
}
function guardarPreferenciaSonido() {
try {
localStorage.setItem(CLAVE_SONIDO, String(estado.sonidoActivo));
} catch (error) {
console.warn("No se pudo guardar la preferencia de sonido.", error);
}
}
/* =====================================================================
5. VOZ (Web Speech API)
===================================================================== */
let vozEspanolElegida = null;
let audioDesbloqueado = false;
function elegirVoz() {
if (!("speechSynthesis" in window)) return;
const voces = window.speechSynthesis.getVoices();
if (!voces || voces.length === 0) return;
vozEspanolElegida =
voces.find(v => v.lang === "es-ES") ||
voces.find(v => v.lang && v.lang.toLowerCase().startsWith("es-es")) ||
voces.find(v => v.lang && v.lang.toLowerCase().startsWith("es")) ||
null;
}
if ("speechSynthesis" in window) {
elegirVoz();
window.speechSynthesis.onvoiceschanged = elegirVoz;
}
// Muchos navegadores móviles (Android/iOS) tardan en cargar la lista de
// voces y solo permiten reproducir audio si ya se cargaron. Reintenta
// brevemente antes de hablar la primera vez.
function vocesListas() {
return new Promise(resolve => {
if (!("speechSynthesis" in window)) { resolve(); return; }
const voces = window.speechSynthesis.getVoices();
if (voces && voces.length > 0) { elegirVoz(); resolve(); return; }
let intentos = 0;
const intervalo = window.setInterval(() => {
const vocesActuales = window.speechSynthesis.getVoices();
intentos += 1;
if ((vocesActuales && vocesActuales.length > 0) || intentos > 12) {
window.clearInterval(intervalo);
elegirVoz();
resolve();
}
}, 150);
});
}
// Los navegadores móviles solo permiten reproducir audio (incluida la
// síntesis de voz) tras una interacción directa del usuario. Esta función
// "desbloquea" el audio en el primer toque/clic de la sesión.
function desbloquearAudio() {
if (audioDesbloqueado || !("speechSynthesis" in window)) return;
audioDesbloqueado = true;
try {
const u = new SpeechSynthesisUtterance(" ");
u.volume = 0;
u.lang = "es-ES";
window.speechSynthesis.speak(u);
window.speechSynthesis.cancel();
} catch (error) {
console.warn("No se pudo desbloquear el audio.", error);
}
}
["pointerdown", "touchend", "click", "keydown"].forEach(evento => {
document.addEventListener(evento, desbloquearAudio, { once: true, passive: true });
});
function detenerVoz() {
if ("speechSynthesis" in window) {
window.speechSynthesis.cancel();
}
}
async function hablar(texto, opciones) {
if (!estado.sonidoActivo || !("speechSynthesis" in window)) return;
await vocesListas();
detenerVoz();
return new Promise(resolve => {
const u = new SpeechSynthesisUtterance(texto);
u.lang = "es-ES";
if (vozEspanolElegida) u.voice = vozEspanolElegida;
u.rate = (opciones && opciones.rate) || 0.95;
u.pitch = (opciones && opciones.pitch) || 1;
u.volume = 1;
let resuelto = false;
const resolverUnaVez = () => { if (!resuelto) { resuelto = true; resolve(); } };
u.onend = resolverUnaVez;
u.onerror = resolverUnaVez;
// Salvaguarda: en algunos navegadores móviles "onend" no llega a
// dispararse; se limita la espera máxima para no bloquear la app.
window.setTimeout(resolverUnaVez, Math.max(2500, texto.length * 220));
estado.sesion.utteranceActual = u;
window.speechSynthesis.speak(u);
});
}
function esperar(ms) {
return new Promise(resolve => window.setTimeout(resolve, ms));
}
/* =====================================================================
6. CONTROL DE SONIDO (interfaz)
===================================================================== */
function actualizarBotonesSonido() {
const icono = estado.sonidoActivo ? "🔊" : "🔇";
[$("boton-sonido-menu"), $("boton-sonido-juego")].forEach(boton => {
boton.textContent = icono;
boton.setAttribute("aria-pressed", String(estado.sonidoActivo));
boton.setAttribute("aria-label", estado.sonidoActivo ? "Sonido activado, pulsar para desactivar" : "Sonido desactivado, pulsar para activar");
});
}
function alternarSonido() {
estado.sonidoActivo = !estado.sonidoActivo;
if (!estado.sonidoActivo) detenerVoz();
guardarPreferenciaSonido();
actualizarBotonesSonido();
anunciar(estado.sonidoActivo ? "Sonido activado" : "Sonido desactivado");
}
$("boton-sonido-menu").addEventListener("click", alternarSonido);
$("boton-sonido-juego").addEventListener("click", alternarSonido);
/* =====================================================================
7. MODAL DE CONFIRMACIÓN
===================================================================== */
function pedirConfirmacion(mensaje, accionConfirmar) {
$("texto-modal").textContent = mensaje;
estado.accionModalConfirmar = accionConfirmar;
$("fondo-modal").classList.remove("oculto");
$("boton-modal-confirmar").focus();
}
function cerrarModal() {
$("fondo-modal").classList.add("oculto");
estado.accionModalConfirmar = null;
}
$("boton-modal-cancelar").addEventListener("click", cerrarModal);
$("boton-modal-confirmar").addEventListener("click", () => {
const accion = estado.accionModalConfirmar;
cerrarModal();
if (typeof accion === "function") accion();
});
$("fondo-modal").addEventListener("keydown", e => {
if (e.key === "Escape") cerrarModal();
});
/* =====================================================================
8. PANTALLA DE MENÚ
===================================================================== */
$("boton-comenzar").addEventListener("click", iniciarSesion);
$("boton-gestionar").addEventListener("click", abrirGestionPalabras);
/* =====================================================================
9. LÓGICA DE LA SESIÓN DE JUEGO
===================================================================== */
function iniciarSesion() {
if (estado.palabras.length === 0) {
anunciar("No hay palabras disponibles. Añade alguna en Gestionar palabras.");
return;
}
const cantidad = Math.min(PALABRAS_POR_SESION, estado.palabras.length);
estado.sesion.palabrasSesion = mezclarArray(estado.palabras).slice(0, cantidad);
estado.sesion.indiceActual = 0;
mostrarPantalla("pantalla-juego");
cargarPalabraActual();
}
function palabraActual() {
return estado.sesion.palabrasSesion[estado.sesion.indiceActual];
}
function actualizarProgreso() {
const total = estado.sesion.palabrasSesion.length;
const actual = estado.sesion.indiceActual + 1;
$("texto-progreso").textContent = `Palabra ${actual} de ${total}`;
$("barra-progreso-interior").style.width = `${(actual / total) * 100}%`;
}
function cargarPalabraActual() {
const palabra = palabraActual();
estado.sesion.colorSeleccionado = null;
actualizarProgreso();
const texto = palabra.silabas.join("").replace(/^\w/, c => c.toUpperCase());
$("texto-palabra-completa").textContent = texto;
$("vista-palabra-completa").classList.remove("oculto");
$("vista-silabas").classList.add("oculto");
$("vista-silabas").innerHTML = "";
$("zona-coloreado").classList.add("oculto");
detenerVoz();
mostrarEfectoAudio(true);
hablar(palabra.silabas.join("")).then(() => {
mostrarEfectoAudio(false);
window.setTimeout(mostrarSilabas, TIEMPO_ANTES_DE_DIVIDIR_MS);
});
}
function mostrarEfectoAudio(activo) {
$("efecto-audio-palabra").classList.toggle("oculto", !activo);
}
function mostrarSilabas() {
const palabra = palabraActual();
$("vista-palabra-completa").classList.add("oculto");
const contenedor = $("vista-silabas");
contenedor.innerHTML = "";
contenedor.classList.remove("oculto");
palabra.silabas.forEach((silaba, indice) => {
if (indice > 0) {
const guion = document.createElement("span");
guion.className = "guion-silaba";
guion.textContent = "-";
guion.setAttribute("aria-hidden", "true");
contenedor.appendChild(guion);
}
const spanSilaba = document.createElement("span");
spanSilaba.className = "silaba";
spanSilaba.textContent = silaba;
spanSilaba.style.color = COLORES_SILABAS_INICIALES[indice % COLORES_SILABAS_INICIALES.length];
spanSilaba.dataset.indice = String(indice);
contenedor.appendChild(spanSilaba);
});
window.setTimeout(pasarAModoColoreado, TIEMPO_TRANSICION_COLOREADO_MS);
}
function pasarAModoColoreado() {
const contenedor = $("vista-silabas");
const spans = contenedor.querySelectorAll(".silaba");
spans.forEach(span => {
span.style.color = "#111827";
span.classList.add("coloreable");
span.style.backgroundColor = "#e5e7eb";
span.setAttribute("tabindex", "0");
span.setAttribute("role", "button");
span.setAttribute("aria-label", `Sílaba ${span.textContent}. Pulsa un color y luego esta sílaba para colorearla.`);
span.addEventListener("click", () => colorearSilaba(span));
span.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
colorearSilaba(span);
}
});
});
generarPaletaGotas();
$("zona-coloreado").classList.remove("oculto");
}
function generarPaletaGotas() {
const contenedor = $("paleta-gotas");
contenedor.innerHTML = "";
const ordenAleatorio = mezclarArray(COLORES_GOTAS);
ordenAleatorio.forEach(color => {
const boton = document.createElement("button");
boton.type = "button";
boton.className = "gota";
boton.style.backgroundColor = color.valor;
boton.dataset.color = color.valor;
boton.setAttribute("aria-label", `Color ${color.nombre}`);
boton.setAttribute("aria-pressed", "false");
const marca = document.createElement("span");
marca.className = "marca-seleccion";
marca.setAttribute("aria-hidden", "true");
boton.appendChild(marca);
boton.addEventListener("click", () => seleccionarColor(boton, color.valor));
contenedor.appendChild(boton);
});
}
function seleccionarColor(botonGota, colorValor) {
document.querySelectorAll(".gota").forEach(g => {
g.classList.remove("seleccionada");
g.setAttribute("aria-pressed", "false");
g.querySelector(".marca-seleccion").textContent = "";
});
botonGota.classList.add("seleccionada");
botonGota.setAttribute("aria-pressed", "true");
botonGota.querySelector(".marca-seleccion").textContent = "✓";
estado.sesion.colorSeleccionado = colorValor;
}
function colorearSilaba(spanSilaba) {
if (!estado.sesion.colorSeleccionado) {
anunciar("Primero elige un color de la paleta.");
return;
}
spanSilaba.style.backgroundColor = estado.sesion.colorSeleccionado;
}
/* ---------- Escuchar sílaba a sílaba ---------- */
$("boton-escuchar-silabas").addEventListener("click", () => {
reproducirSecuenciaSilabas();
});
async function reproducirSecuenciaSilabas() {
if (estado.sesion.reproduciendoSecuencia) {
detenerVoz();
estado.sesion.reproduciendoSecuencia = false;
await esperar(80);
}
estado.sesion.reproduciendoSecuencia = true;
const spans = Array.from(document.querySelectorAll("#vista-silabas .silaba"));
const idExecucion = Symbol();
estado.sesion._idEjecucionActual = idExecucion;
for (const span of spans) {
if (estado.sesion._idEjecucionActual !== idExecucion) return;
spans.forEach(s => s.classList.remove("resaltada"));
span.classList.add("resaltada");
await hablar(span.textContent);
if (estado.sesion._idEjecucionActual !== idExecucion) return;
await esperar(PAUSA_ENTRE_SILABAS_MS);
}
spans.forEach(s => s.classList.remove("resaltada"));
if (estado.sesion._idEjecucionActual !== idExecucion) return;
mostrarEfectoAudio(true);
const palabra = palabraActual();
await hablar(palabra.silabas.join(""));
mostrarEfectoAudio(false);
estado.sesion.reproduciendoSecuencia = false;
}
/* ---------- Palabra terminada ---------- */
$("boton-palabra-terminada").addEventListener("click", finalizarPalabraActual);
function finalizarPalabraActual() {
detenerVoz();
mostrarRefuerzoMotivador();
}
function mostrarRefuerzoMotivador() {
const mensaje = MENSAJES_MOTIVADORES[Math.floor(Math.random() * MENSAJES_MOTIVADORES.length)];
const emoji = EMOJIS_REFUERZO[Math.floor(Math.random() * EMOJIS_REFUERZO.length)];
$("texto-mensaje-motivador").textContent = mensaje;
$("emoji-refuerzo").textContent = emoji;
mostrarPantalla("pantalla-refuerzo");
anunciar(mensaje);
if (!prefiereMenosMovimiento) {
lanzarConfeti();
}
window.setTimeout(avanzarSiguientePalabra, TIEMPO_ANTES_SIGUIENTE_PALABRA_MS);
}
function avanzarSiguientePalabra() {
estado.sesion.indiceActual += 1;
if (estado.sesion.indiceActual >= estado.sesion.palabrasSesion.length) {
mostrarPantallaFinal();
} else {
mostrarPantalla("pantalla-juego");
cargarPalabraActual();
}
}
function mostrarPantallaFinal() {
const total = estado.sesion.palabrasSesion.length;
$("texto-resumen-final").textContent = `Has completado ${total} de ${total} palabras. ¡Excelente trabajo!`;
mostrarPantalla("pantalla-final");
anunciar("Sesión completada. Enhorabuena.");
if (!prefiereMenosMovimiento) lanzarConfeti();
}
$("boton-volver-a-jugar").addEventListener("click", iniciarSesion);
$("boton-ir-a-menu-final").addEventListener("click", () => mostrarPantalla("pantalla-menu"));
/* ---------- Volver al menú desde el juego (con confirmación) ---------- */
$("boton-menu-desde-juego").addEventListener("click", () => {
pedirConfirmacion(
"Todavía hay una actividad en curso. Si vuelves al menú perderás el progreso de esta partida. ¿Deseas continuar?",
() => {
detenerVoz();
estado.sesion.reproduciendoSecuencia = false;
mostrarPantalla("pantalla-menu");
}
);
});
/* =====================================================================
10. CONFETI (respeta prefers-reduced-motion)
===================================================================== */
function lanzarConfeti() {
const capa = document.createElement("div");
capa.className = "capa-confeti";
capa.setAttribute("aria-hidden", "true");
const coloresConfeti = ["#e53935","#fb8c00","#fdd835","#43a047","#1e88e5","#8e24aa","#ec407a"];
const cantidad = 40;
for (let i = 0; i < cantidad; i++) {
const trozo = document.createElement("span");
trozo.className = "trozo-confeti";
trozo.style.left = `${Math.random() * 100}vw`;
trozo.style.backgroundColor = coloresConfeti[i % coloresConfeti.length];
const duracion = 1.6 + Math.random() * 1.2;
trozo.style.animationDuration = `${duracion}s`;
trozo.style.animationDelay = `${Math.random() * 0.4}s`;
capa.appendChild(trozo);
}
document.body.appendChild(capa);
window.setTimeout(() => capa.remove(), 3200);
}
/* =====================================================================
11. GESTIÓN DE PALABRAS
===================================================================== */
function abrirGestionPalabras() {
estado.edicion.activo = false;
estado.edicion.indiceOriginal = null;
$("campo-silabas").value = "";
$("boton-cancelar-edicion").classList.add("oculto");
$("boton-guardar-palabra").textContent = "💾 Guardar palabra";
$("aviso-guardado").textContent = "";
renderizarListaPalabras();
mostrarPantalla("pantalla-gestion");
}
$("boton-volver-menu-gestion").addEventListener("click", () => mostrarPantalla("pantalla-menu"));
function renderizarListaPalabras() {
const lista = $("lista-palabras");
lista.innerHTML = "";
$("contador-palabras").textContent = String(estado.palabras.length);
estado.palabras.forEach((palabra, indice) => {
const li = document.createElement("li");
li.className = "fila-palabra";
const textoSpan = document.createElement("span");
textoSpan.className = "texto-palabra";
textoSpan.textContent = palabra.silabas.join("-");
const etiqueta = document.createElement("span");
etiqueta.className = "etiqueta-origen";
etiqueta.textContent = palabra.origen === "inicial" ? "Original" : "Añadida";
textoSpan.appendChild(etiqueta);
const acciones = document.createElement("div");
acciones.className = "acciones-palabra";
const botonEditar = document.createElement("button");
botonEditar.type = "button";
botonEditar.className = "boton-secundario";
botonEditar.textContent = "✏️ Editar";
botonEditar.setAttribute("aria-label", `Editar la palabra ${palabra.silabas.join("")}`);
botonEditar.addEventListener("click", () => iniciarEdicionPalabra(indice));
const botonEliminar = document.createElement("button");
botonEliminar.type = "button";
botonEliminar.className = "boton-peligro";
botonEliminar.textContent = "🗑️ Eliminar";
botonEliminar.setAttribute("aria-label", `Eliminar la palabra ${palabra.silabas.join("")}`);
botonEliminar.addEventListener("click", () => solicitarEliminarPalabra(indice));
acciones.appendChild(botonEditar);
acciones.appendChild(botonEliminar);
li.appendChild(textoSpan);
li.appendChild(acciones);
lista.appendChild(li);
});
}
function validarEntradaSilabas(texto) {
const limpio = texto.trim().toLowerCase();
if (!limpio) return { valido: false, error: "Escribe una palabra con sus sílabas separadas por guiones." };
if (!/^[a-záéíóúüñ]+(-[a-záéíóúüñ]+)+$/i.test(limpio)) {
return { valido: false, error: "Formato no válido. Usa solo letras y guiones, por ejemplo: ma-ri-po-sa" };
}
const silabas = limpio.split("-").filter(Boolean);
if (silabas.length < 2) {
return { valido: false, error: "La palabra debe tener al menos dos sílabas separadas por un guion." };
}
return { valido: true, silabas };
}
$("boton-guardar-palabra").addEventListener("click", () => {
const resultado = validarEntradaSilabas($("campo-silabas").value);
const aviso = $("aviso-guardado");
if (!resultado.valido) {
aviso.style.color = "var(--color-peligro)";
aviso.textContent = resultado.error;
return;
}
const nuevaPalabraTexto = normalizarPalabra(resultado.silabas);
const duplicadoIndice = estado.palabras.findIndex((p, i) =>
normalizarPalabra(p.silabas) === nuevaPalabraTexto &&
i !== estado.edicion.indiceOriginal
);
if (duplicadoIndice !== -1) {
aviso.style.color = "var(--color-peligro)";
aviso.textContent = "Esa palabra ya existe en el banco.";
return;
}
if (estado.edicion.activo && estado.edicion.indiceOriginal !== null) {
const origenPrevio = estado.palabras[estado.edicion.indiceOriginal].origen;
estado.palabras[estado.edicion.indiceOriginal] = { silabas: resultado.silabas, origen: origenPrevio };
aviso.style.color = "var(--color-exito)";
aviso.textContent = "Palabra actualizada correctamente.";
} else {
estado.palabras.push({ silabas: resultado.silabas, origen: "personalizada" });
aviso.style.color = "var(--color-exito)";
aviso.textContent = "Palabra añadida correctamente.";
}
guardarPalabras();
finalizarEdicion();
renderizarListaPalabras();
});
function iniciarEdicionPalabra(indice) {
estado.edicion.activo = true;
estado.edicion.indiceOriginal = indice;
$("campo-silabas").value = estado.palabras[indice].silabas.join("-");
$("boton-cancelar-edicion").classList.remove("oculto");
$("boton-guardar-palabra").textContent = "💾 Guardar cambios";
$("aviso-guardado").textContent = "";
$("campo-silabas").focus();
}
function finalizarEdicion() {
estado.edicion.activo = false;
estado.edicion.indiceOriginal = null;
$("campo-silabas").value = "";
$("boton-cancelar-edicion").classList.add("oculto");
$("boton-guardar-palabra").textContent = "💾 Guardar palabra";
}
$("boton-cancelar-edicion").addEventListener("click", finalizarEdicion);
function solicitarEliminarPalabra(indice) {
const palabra = estado.palabras[indice];
pedirConfirmacion(
`¿Seguro que quieres eliminar la palabra "${palabra.silabas.join("")}"?`,
() => {
estado.palabras.splice(indice, 1);
if (estado.edicion.indiceOriginal === indice) finalizarEdicion();
guardarPalabras();
renderizarListaPalabras();
anunciar("Palabra eliminada.");
}
);
}
$("boton-restaurar-banco").addEventListener("click", () => {
pedirConfirmacion(
"Esto eliminará las palabras añadidas o modificadas y recuperará el banco de palabras original. ¿Deseas continuar?",
() => {
estado.palabras = BANCO_INICIAL.map(p => ({ silabas: p.silabas.slice(), origen: p.origen }));
guardarPalabras();
finalizarEdicion();
renderizarListaPalabras();
$("aviso-guardado").style.color = "var(--color-exito)";
$("aviso-guardado").textContent = "Banco de palabras restaurado.";
anunciar("Banco de palabras restaurado.");
}
);
});
/* =====================================================================
12. INICIALIZACIÓN
===================================================================== */
function inicializar() {
cargarPreferenciaSonido();
cargarPalabras();
actualizarBotonesSonido();
mostrarPantalla("pantalla-menu");
}
inicializar();
})();
</script>
</body>
</html>