| 1 | /** |
| 2 | * MAPS Connect - Estilos de la página |
| 3 | * Permite elegir un estilo visual global (skin) que afecta a toda la |
| 4 | * aplicación (sidebar, topbar, tarjetas, botones, formularios, fondo...). |
| 5 | * Guarda la preferencia en localStorage y la aplica como atributo |
| 6 | * "data-estilo" sobre <html>; frontend/css/estilos.css define los skins |
| 7 | * bajo html[data-estilo="..."]. |
| 8 | * |
| 9 | * Para añadir un estilo nuevo solo hay que: |
| 10 | * 1) Agregar su entrada en ESTILOS (id, nombre, descripcion, preview). |
| 11 | * 2) Escribir su bloque CSS en estilos.css bajo html[data-estilo="id"]. |
| 12 | * 3) Si el estilo inyecta su propio fondo (como "cyber"), añadir la |
| 13 | * lógica de inyección en _ponFondo/_limpiarFondo. |
| 14 | */ |
| 15 | const Estilos = { |
| 16 | KEY: 'estilo-maps', |
| 17 | |
| 18 | STYLES: [ |
| 19 | { |
| 20 | id: 'clasico', |
| 21 | nombre: 'Clásico', |
| 22 | descripcion: 'Estilo predeterminado de MAPS Connect con la paleta institucional.', |
| 23 | preview: 'linear-gradient(135deg, #087527 0%, #3fb950 60%, #a3e635 100%)' |
| 24 | }, |
| 25 | { |
| 26 | id: 'cyber', |
| 27 | nombre: 'Cyberpunk Network', |
| 28 | descripcion: 'Red neuronal de neón cian y magenta sobre fondo de terminal oscura.', |
| 29 | preview: 'linear-gradient(135deg, #00f0ff 0%, #1a0033 50%, #ff007f 100%)' |
| 30 | }, |
| 31 | { |
| 32 | id: 'medieval', |
| 33 | nombre: 'Hogwarts Medieval', |
| 34 | descripcion: 'Oro y pergamino sobre la fortaleza mágica (experiencia 3D tipo Hogwarts).', |
| 35 | preview: 'linear-gradient(135deg, #d4af37 0%, #18110b 50%, #5a1414 100%)' |
| 36 | }, |
| 37 | { |
| 38 | id: 'saiyan', |
| 39 | nombre: 'Saiyan Power Edition', |
| 40 | descripcion: 'Nube Kinto, estelas de ki y aura dorada sobre el dojo de los Z-Warriors (naranja intenso).', |
| 41 | preview: 'linear-gradient(135deg, #f97316 0%, #1e3a8a 50%, #facc15 100%)' |
| 42 | }, |
| 43 | { |
| 44 | id: 'noir', |
| 45 | nombre: 'Cyber Noir Edition', |
| 46 | descripcion: 'Callejón nocturno con neón rojo, lluvia digital y expedientes confidenciales (gótico cyber).', |
| 47 | preview: 'linear-gradient(135deg, #e11d48 0%, #080305 50%, #3a0011 100%)' |
| 48 | }, |
| 49 | { |
| 50 | id: 'alchemy', |
| 51 | nombre: 'Alchemical Codex Edition', |
| 52 | descripcion: 'Astrolabio, geometría sagrada y oro de filósofo sobre la cámara hermetista (esmeralda).', |
| 53 | preview: 'linear-gradient(135deg, #10b981 0%, #020d08 50%, #fbbf24 100%)' |
| 54 | }, |
| 55 | { |
| 56 | id: 'minimal', |
| 57 | nombre: 'Minimalist Tricolor Edition', |
| 58 | descripcion: 'Geometría suiza azul, rojo y blanco con retícula fina y parallax pausado (estilo internacional).', |
| 59 | preview: 'linear-gradient(135deg, #2563eb 0%, #f8fafc 50%, #dc2626 100%)' |
| 60 | }, |
| 61 | { |
| 62 | id: 'invernadero', |
| 63 | nombre: 'Invernadero', |
| 64 | descripcion: 'Cubierta vegetal, foliaje mecido y luz tamizada sobre una experiencia académica que crece (verde institucional).', |
| 65 | preview: 'linear-gradient(135deg, #3E6B4F 0%, #E7EEDA 50%, #C97B4A 100%)' |
| 66 | } |
| 67 | ], |
| 68 | |
| 69 | _VARIABLES_ACCENTO: ['--accent', '--accent-dark', '--accent-mid', '--accent-bright', |
| 70 | '--accent-deep', '--accent-soft', '--accent-soft-border', '--accent-contrast'], |
| 71 | |
| 72 | /** Textos personalizados por skin (botones, enlaces y placeholders). |
| 73 | Las claves son el texto original exacto y los valores el texto temático. */ |
| 74 | TEXTOS_ESTILO: { |
| 75 | cyber: {}, |
| 76 | medieval: { |
| 77 | 'Publicar duda': 'Enviar al Palacio', |
| 78 | 'Publicar Duda': 'Enviar al Palacio', |
| 79 | 'Publicar': 'Enviar al Palacio', |
| 80 | 'Publicar de todos modos': 'Enviar de todos modos', |
| 81 | 'Cerrar sesión': 'Abandonar el Castillo', |
| 82 | 'Guardar': 'Guardar en el Pergamino', |
| 83 | 'Enviar Mensaje Privado': 'Enviar por Búho', |
| 84 | '+ Crear sesión de repaso': '+ Convocar Círculo de Estudio', |
| 85 | 'Crear sesión': 'Convocar Estudio', |
| 86 | 'Unirme': 'Unirme al Círculo', |
| 87 | '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Lanza tu pregón académico a la Comunidad del Castillo...' |
| 88 | }, |
| 89 | saiyan: { |
| 90 | 'Publicar duda': 'Lanzar Ataque Ki', |
| 91 | 'Publicar Duda': 'Lanzar Ataque Ki', |
| 92 | 'Publicar': 'Lanzar Ataque Ki', |
| 93 | 'Publicar de todos modos': 'Lanzar de todos modos', |
| 94 | 'Cerrar sesión': 'Descansar / Salir', |
| 95 | 'Guardar': 'Guardar Energía', |
| 96 | 'Enviar Mensaje Privado': 'Telepatía Instantánea', |
| 97 | '+ Crear sesión de repaso': '+ Crear Sala de Entrenamiento', |
| 98 | 'Crear sesión': 'Crear Entrenamiento', |
| 99 | 'Unirme': 'Entrar al Dojo', |
| 100 | '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Lanza tu duda o reto técnico al campo de entrenamiento...' |
| 101 | }, |
| 102 | noir: { |
| 103 | 'Publicar duda': 'Registrar Pista', |
| 104 | 'Publicar Duda': 'Registrar Pista', |
| 105 | 'Publicar': 'Registrar Pista', |
| 106 | 'Publicar de todos modos': 'Registrar de todos modos', |
| 107 | 'Cerrar sesión': 'Cerrar Expediente', |
| 108 | 'Guardar': 'Sellar en Archivo', |
| 109 | 'Enviar Mensaje Privado': 'Enviar Nota Cifrada', |
| 110 | '+ Crear sesión de repaso': '+ Convocar Ronda de Estudio', |
| 111 | 'Crear sesión': 'Convocar Ronda', |
| 112 | 'Unirme': 'Entrar a la Ronda', |
| 113 | '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Inscribe una pista, duda o cabo suelto al expediente...' |
| 114 | }, |
| 115 | alchemy: { |
| 116 | 'Publicar duda': 'Liberar Sello', |
| 117 | 'Publicar Duda': 'Liberar Sello', |
| 118 | 'Publicar': 'Liberar Sello', |
| 119 | 'Publicar de todos modos': 'Liberar de todos modos', |
| 120 | 'Cerrar sesión': 'Cerrar Grimorio / Salir', |
| 121 | 'Guardar': 'Sellar en Papiro', |
| 122 | 'Enviar Mensaje Privado': 'Enviar Sello Privado', |
| 123 | '+ Crear sesión de repaso': '+ Convocar Círculo Esotérico', |
| 124 | 'Crear sesión': 'Convocar Círculo', |
| 125 | 'Unirme': 'Unirme al Círculo', |
| 126 | '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Inscribe una consulta o runa técnica al Códice...' |
| 127 | }, |
| 128 | minimal: { |
| 129 | 'Publicar duda': 'Enviar Solicitud', |
| 130 | 'Publicar Duda': 'Enviar Solicitud', |
| 131 | 'Publicar': 'Enviar Solicitud', |
| 132 | 'Publicar de todos modos': 'Enviar de todos modos', |
| 133 | 'Cerrar sesión': 'Cerrar Sesión', |
| 134 | 'Guardar': 'Guardar Cambios', |
| 135 | 'Enviar Mensaje Privado': 'Enviar Mensaje', |
| 136 | '+ Crear sesión de repaso': '+ Programar Sesión', |
| 137 | 'Crear sesión': 'Programar Sesión', |
| 138 | 'Unirme': 'Unirme a la Sesión', |
| 139 | '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Registra tu duda académica en el sistema...' |
| 140 | }, |
| 141 | invernadero: { |
| 142 | 'Publicar duda': 'Plantar la consulta', |
| 143 | 'Publicar Duda': 'Plantar la consulta', |
| 144 | 'Publicar': 'Plantar la consulta', |
| 145 | 'Publicar de todos modos': 'Plantar de todos modos', |
| 146 | 'Cerrar sesión': 'Regar y cerrar sesión', |
| 147 | 'Guardar': 'Guardar en el semillero', |
| 148 | 'Enviar Mensaje Privado': 'Enviar rama privada', |
| 149 | '+ Crear sesión de repaso': '+ Crear sesión de cultivo', |
| 150 | 'Crear sesión': 'Crear sesión de cultivo', |
| 151 | 'Unirme': 'Unirme al cultivo', |
| 152 | '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Siembra una duda o nota para tu comunidad...' |
| 153 | } |
| 154 | }, |
| 155 | |
| 156 | guardado() { |
| 157 | try { return localStorage.getItem(this.KEY) || 'clasico'; } catch (e) { return 'clasico'; } |
| 158 | }, |
| 159 | |
| 160 | /** Estilo activo en este momento (sin consultar storage). */ |
| 161 | activo() { |
| 162 | return document.documentElement.getAttribute('data-estilo') || 'clasico'; |
| 163 | }, |
| 164 | |
| 165 | /* ------------------------------------------------------------ |
| 166 | Fondos de estilo (elementos inyectados al final de <body>) |
| 167 | ------------------------------------------------------------ */ |
| 168 | |
| 169 | _cyberSVG() { |
| 170 | return ` |
| 171 | <svg class="cyber-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true"> |
| 172 | <rect width="1400" height="900" fill="#010204"/> |
| 173 | <g class="data-link"> |
| 174 | <line x1="200" y1="200" x2="500" y2="350"/> |
| 175 | <line x1="500" y1="350" x2="700" y2="200"/> |
| 176 | <line x1="700" y1="200" x2="1000" y2="300"/> |
| 177 | <line x1="1000" y1="300" x2="1200" y2="600"/> |
| 178 | <line x1="500" y1="350" x2="600" y2="650"/> |
| 179 | <line x1="600" y1="650" x2="300" y2="750"/> |
| 180 | <line x1="300" y1="750" x2="200" y2="200"/> |
| 181 | <line x1="700" y1="200" x2="850" y2="500"/> |
| 182 | <line x1="850" y1="500" x2="1200" y2="600"/> |
| 183 | <line x1="500" y1="350" x2="850" y2="500"/> |
| 184 | </g> |
| 185 | <circle class="data-node" cx="200" cy="200" r="6"/> |
| 186 | <circle class="data-node" cx="1000" cy="300" r="5"/> |
| 187 | <circle class="data-node" cx="600" cy="650" r="6"/> |
| 188 | <circle class="data-node" cx="300" cy="750" r="5"/> |
| 189 | <circle class="data-node" cx="1200" cy="600" r="7"/> |
| 190 | <circle class="core-node" cx="700" cy="350" r="16"/> |
| 191 | <circle class="data-node" cx="500" cy="350" r="8"/> |
| 192 | <circle class="data-node" cx="700" cy="200" r="8"/> |
| 193 | <circle class="data-node" cx="850" cy="500" r="9"/> |
| 194 | </svg>`; |
| 195 | }, |
| 196 | |
| 197 | /** Mapa de página -> posición de cámara (pan/zoom) sobre el skyline del |
| 198 | castillo. dx/dy en píxeles sobre la capa de 2600x1100 y escala. */ |
| 199 | _castilloZonas() { |
| 200 | return { |
| 201 | 'inicio.html': { dx: 0, dy: 40, scale: 0.62 }, |
| 202 | 'perfil.html': { dx: 1000, dy: -60, scale: 1.15 }, |
| 203 | 'certificados.html': { dx: 650, dy: -40, scale: 1.1 }, |
| 204 | 'comunidad.html': { dx: 300, dy: -20, scale: 1.05 }, |
| 205 | 'mensajes.html': { dx: -300, dy: -30, scale: 1.15 }, |
| 206 | 'empresarial.html': { dx: -650, dy: -10, scale: 1.05 }, |
| 207 | 'circulos.html': { dx: -950, dy: -30, scale: 1.1 }, |
| 208 | 'ajustes.html': { dx: -1200, dy: -20, scale: 1.2 }, |
| 209 | 'admin.html': { dx: 0, dy: 60, scale: 1.25 }, |
| 210 | 'docente.html': { dx: 0, dy: 40, scale: 1.15 } |
| 211 | }; |
| 212 | }, |
| 213 | |
| 214 | /** Skyline panorámico del castillo (2600x1100) con zonas por sección. */ |
| 215 | _castleSkyline() { |
| 216 | return ` |
| 217 | <svg class="castle-layer" id="castleLayer" viewBox="0 0 2600 1100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> |
| 218 | <defs> |
| 219 | <linearGradient id="skyGrad" x1="0" y1="0" x2="0" y2="1"> |
| 220 | <stop offset="0%" stop-color="#1C0F0B"/> |
| 221 | <stop offset="100%" stop-color="#0E0806"/> |
| 222 | </linearGradient> |
| 223 | <linearGradient id="stoneGrad" x1="0" y1="0" x2="0" y2="1"> |
| 224 | <stop offset="0%" stop-color="#4A2E24"/> |
| 225 | <stop offset="100%" stop-color="#2A1A15"/> |
| 226 | </linearGradient> |
| 227 | </defs> |
| 228 | |
| 229 | <rect width="2600" height="1100" fill="url(#skyGrad)"/> |
| 230 | <circle cx="1300" cy="180" r="90" fill="#3A2C1E" opacity="0.5"/> |
| 231 | |
| 232 | <g fill="#180E0A" opacity="0.7"> |
| 233 | <polygon points="0,500 200,380 400,500 600,410 800,500 1000,430 1200,500 1400,420 1600,500 1800,440 2000,500 2200,410 2400,500 2600,440 2600,700 0,700"/> |
| 234 | </g> |
| 235 | |
| 236 | <g id="zone-perfil"> |
| 237 | <rect x="220" y="480" width="160" height="420" fill="url(#stoneGrad)"/> |
| 238 | <polygon points="200,480 300,380 400,480" fill="#3E1717"/> |
| 239 | <rect x="270" y="560" width="60" height="90" fill="#0E0806"/> |
| 240 | <rect x="245" y="700" width="30" height="50" fill="#0E0806"/> |
| 241 | <rect x="325" y="700" width="30" height="50" fill="#0E0806"/> |
| 242 | <rect x="290" y="360" width="20" height="40" fill="#A9834B"/> |
| 243 | <polygon points="290,360 300,335 310,360" fill="#A9834B"/> |
| 244 | </g> |
| 245 | |
| 246 | <g id="zone-ruta"> |
| 247 | <rect x="560" y="440" width="180" height="460" fill="url(#stoneGrad)"/> |
| 248 | <polygon points="540,440 650,320 760,440" fill="#3E1717"/> |
| 249 | <circle cx="650" cy="520" r="34" fill="#0E0806"/> |
| 250 | <circle cx="650" cy="520" r="24" fill="#A9834B" opacity="0.3"/> |
| 251 | <rect x="600" y="720" width="30" height="55" fill="#0E0806"/> |
| 252 | <rect x="670" y="720" width="30" height="55" fill="#0E0806"/> |
| 253 | <rect x="640" y="300" width="20" height="35" fill="#A9834B"/> |
| 254 | <polygon points="640,300 650,278 660,300" fill="#A9834B"/> |
| 255 | <path d="M615,470 L685,470 M650,470 L650,510" stroke="#A9834B" stroke-width="2" opacity="0.4"/> |
| 256 | </g> |
| 257 | |
| 258 | <g id="zone-comunidad"> |
| 259 | <rect x="900" y="560" width="420" height="340" fill="url(#stoneGrad)"/> |
| 260 | <polygon points="880,560 1110,470 1340,560" fill="#3E1717"/> |
| 261 | <circle cx="960" cy="640" r="26" fill="#0E0806"/> |
| 262 | <circle cx="1040" cy="640" r="26" fill="#0E0806"/> |
| 263 | <circle cx="1120" cy="640" r="26" fill="#0E0806"/> |
| 264 | <circle cx="1200" cy="640" r="26" fill="#0E0806"/> |
| 265 | <circle cx="1280" cy="640" r="26" fill="#0E0806"/> |
| 266 | <rect x="950" y="760" width="45" height="60" fill="#0E0806"/> |
| 267 | <rect x="1240" y="760" width="45" height="60" fill="#0E0806"/> |
| 268 | </g> |
| 269 | |
| 270 | <g id="zone-inicio"> |
| 271 | <rect x="1180" y="330" width="240" height="570" fill="url(#stoneGrad)"/> |
| 272 | <polygon points="1150,330 1300,180 1450,330" fill="#3E1717"/> |
| 273 | <rect x="1270" y="150" width="60" height="60" fill="#3E1717"/> |
| 274 | <polygon points="1270,150 1300,110 1330,150" fill="#5C2323"/> |
| 275 | <rect x="1290" y="90" width="10" height="40" fill="#A9834B"/> |
| 276 | <polygon points="1300,90 1330,105 1300,110" fill="#5C2323"/> |
| 277 | <circle cx="1300" cy="430" r="46" fill="#0E0806"/> |
| 278 | <circle cx="1300" cy="430" r="32" fill="#A9834B" opacity="0.25"/> |
| 279 | <rect x="1225" y="560" width="60" height="90" fill="#0E0806"/> |
| 280 | <rect x="1315" y="560" width="60" height="90" fill="#0E0806"/> |
| 281 | <rect x="1250" y="760" width="100" height="140" fill="#0E0806"/> |
| 282 | <polygon points="1250,760 1300,715 1350,760" fill="#180E0A"/> |
| 283 | <rect x="900" y="850" width="800" height="30" fill="#241512"/> |
| 284 | <g fill="#241512"> |
| 285 | <rect x="900" y="826" width="20" height="24"/><rect x="940" y="826" width="20" height="24"/> |
| 286 | <rect x="980" y="826" width="20" height="24"/><rect x="1020" y="826" width="20" height="24"/> |
| 287 | <rect x="1060" y="826" width="20" height="24"/><rect x="1100" y="826" width="20" height="24"/> |
| 288 | <rect x="1140" y="826" width="20" height="24"/><rect x="1180" y="826" width="20" height="24"/> |
| 289 | <rect x="1220" y="826" width="20" height="24"/><rect x="1260" y="826" width="20" height="24"/> |
| 290 | <rect x="1300" y="826" width="20" height="24"/><rect x="1340" y="826" width="20" height="24"/> |
| 291 | <rect x="1380" y="826" width="20" height="24"/><rect x="1420" y="826" width="20" height="24"/> |
| 292 | <rect x="1460" y="826" width="20" height="24"/><rect x="1500" y="826" width="20" height="24"/> |
| 293 | <rect x="1540" y="826" width="20" height="24"/><rect x="1580" y="826" width="20" height="24"/> |
| 294 | <rect x="1620" y="826" width="20" height="24"/><rect x="1660" y="826" width="20" height="24"/> |
| 295 | </g> |
| 296 | </g> |
| 297 | |
| 298 | <g id="zone-mensajes"> |
| 299 | <rect x="1700" y="470" width="150" height="430" fill="url(#stoneGrad)"/> |
| 300 | <polygon points="1680,470 1775,370 1870,470" fill="#3E1717"/> |
| 301 | <rect x="1745" y="540" width="24" height="70" fill="#0E0806"/> |
| 302 | <rect x="1785" y="540" width="24" height="70" fill="#0E0806"/> |
| 303 | <rect x="1730" y="700" width="30" height="50" fill="#0E0806"/> |
| 304 | <rect x="1795" y="700" width="30" height="50" fill="#0E0806"/> |
| 305 | <path d="M1700,500 L1660,470 M1870,500 L1910,470" stroke="#A9834B" stroke-width="2" opacity="0.5"/> |
| 306 | <ellipse cx="1660" cy="465" rx="10" ry="6" fill="#A9834B" opacity="0.5"/> |
| 307 | <ellipse cx="1910" cy="465" rx="10" ry="6" fill="#A9834B" opacity="0.5"/> |
| 308 | </g> |
| 309 | |
| 310 | <g id="zone-empresarial"> |
| 311 | <rect x="1970" y="600" width="130" height="300" fill="url(#stoneGrad)"/> |
| 312 | <rect x="2110" y="560" width="130" height="340" fill="url(#stoneGrad)"/> |
| 313 | <rect x="2250" y="600" width="130" height="300" fill="url(#stoneGrad)"/> |
| 314 | <polygon points="1960,600 2035,545 2110,600" fill="#3E1717"/> |
| 315 | <polygon points="2100,560 2175,500 2250,560" fill="#5C2323"/> |
| 316 | <polygon points="2240,600 2315,545 2390,600" fill="#3E1717"/> |
| 317 | <rect x="2000" y="700" width="30" height="45" fill="#0E0806"/> |
| 318 | <rect x="2150" y="680" width="30" height="45" fill="#0E0806"/> |
| 319 | <rect x="2300" y="700" width="30" height="45" fill="#0E0806"/> |
| 320 | <rect x="1990" y="900" width="420" height="14" fill="#A9834B" opacity="0.35"/> |
| 321 | </g> |
| 322 | |
| 323 | <g id="zone-circulos"> |
| 324 | <rect x="2280" y="500" width="220" height="400" fill="url(#stoneGrad)"/> |
| 325 | <circle cx="2390" cy="480" r="120" fill="#3E1717"/> |
| 326 | <circle cx="2390" cy="480" r="90" fill="#2A1512"/> |
| 327 | <circle cx="2390" cy="480" r="16" fill="#A9834B" opacity="0.4"/> |
| 328 | <rect x="2330" y="720" width="30" height="55" fill="#0E0806"/> |
| 329 | <rect x="2420" y="720" width="30" height="55" fill="#0E0806"/> |
| 330 | </g> |
| 331 | |
| 332 | <g id="zone-ajustes"> |
| 333 | <rect x="2470" y="560" width="110" height="340" fill="url(#stoneGrad)"/> |
| 334 | <polygon points="2455,560 2525,480 2595,560" fill="#3E1717"/> |
| 335 | <rect x="2500" y="620" width="50" height="60" fill="#0E0806"/> |
| 336 | <rect x="2495" y="750" width="26" height="40" fill="#0E0806"/> |
| 337 | <rect x="2534" y="750" width="26" height="40" fill="#0E0806"/> |
| 338 | <rect x="2515" y="450" width="18" height="34" fill="#A9834B"/> |
| 339 | <polygon points="2515,450 2524,430 2533,450" fill="#A9834B"/> |
| 340 | </g> |
| 341 | |
| 342 | <g fill="#A9834B" opacity="0.4"> |
| 343 | <circle cx="150" cy="90" r="2"/><circle cx="480" cy="60" r="2"/> |
| 344 | <circle cx="900" cy="100" r="2"/><circle cx="1700" cy="70" r="2"/> |
| 345 | <circle cx="2100" cy="110" r="2"/><circle cx="2450" cy="80" r="2"/> |
| 346 | </g> |
| 347 | </svg>`; |
| 348 | }, |
| 349 | |
| 350 | /** Posiciona la cámara del castillo según la sección activa del menú. */ |
| 351 | _panCastilloZona() { |
| 352 | const layer = document.querySelector('.castle-layer'); |
| 353 | if (!layer) return; |
| 354 | const activo = document.querySelector('#sidebar .sidebar-nav a.active, .sidebar-nav a.active'); |
| 355 | const href = activo ? activo.getAttribute('href') : 'inicio.html'; |
| 356 | const zona = (this._castilloZonas()[href] || this._castilloZonas()['inicio.html']); |
| 357 | layer.style.transform = `translate(${zona.dx}px, ${zona.dy}px) scale(${zona.scale})`; |
| 358 | }, |
| 359 | |
| 360 | /** Re-encuadra la cámara del castillo cuando el menú queda listo, al |
| 361 | cambiar de skin y al navegar (la página activa determina la zona). */ |
| 362 | _bindCastilloPan() { |
| 363 | if (this._castlePanBinded) { |
| 364 | this._panCastilloZona(); |
| 365 | return; |
| 366 | } |
| 367 | this._castlePanBinded = true; |
| 368 | document.addEventListener('estilos:cambio', () => this._panCastilloZona()); |
| 369 | if (document.readyState === 'loading') { |
| 370 | document.addEventListener('DOMContentLoaded', () => this._panCastilloZona(), { once: true }); |
| 371 | } else { |
| 372 | this._panCastilloZona(); |
| 373 | } |
| 374 | }, |
| 375 | |
| 376 | _saiyanSVG() { |
| 377 | return ` |
| 378 | <svg class="saiyan-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true"> |
| 379 | <rect width="1400" height="900" fill="#f97316"/> |
| 380 | |
| 381 | <path class="kintoun-cloud" d="M300,500 Q350,420 450,450 Q520,380 620,430 Q700,390 780,460 Q880,440 920,520 Q1000,530 980,620 Q900,680 300,680 Z"/> |
| 382 | |
| 383 | <path class="ki-energy-trail" d="M100,100 L1300,800"/> |
| 384 | <path class="ki-energy-trail" d="M200,800 L1200,100"/> |
| 385 | <path class="ki-energy-trail" d="M0,450 L1400,450"/> |
| 386 | </svg>`; |
| 387 | }, |
| 388 | |
| 389 | _noirSVG() { |
| 390 | return ` |
| 391 | <svg class="noir-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true"> |
| 392 | <rect width="1400" height="900" fill="#080305"/> |
| 393 | <path d="M0,700 L250,500 L450,550 L750,380 L1050,480 L1400,400 L1400,900 L0,900 Z" fill="#0d0407"/> |
| 394 | <path class="neon-sign" d="M300,320 L350,280 L400,320 L350,360 Z"/> |
| 395 | <path class="neon-sign" d="M950,250 L1020,250 L1020,310 L950,310 Z"/> |
| 396 | <g class="rain-streak"> |
| 397 | <line x1="100" y1="0" x2="60" y2="150"/> |
| 398 | <line x1="300" y1="0" x2="260" y2="150"/> |
| 399 | <line x1="500" y1="0" x2="460" y2="150"/> |
| 400 | <line x1="700" y1="0" x2="660" y2="150"/> |
| 401 | <line x1="900" y1="0" x2="860" y2="150"/> |
| 402 | <line x1="1100" y1="0" x2="1060" y2="150"/> |
| 403 | <line x1="1300" y1="0" x2="1260" y2="150"/> |
| 404 | </g> |
| 405 | </svg>`; |
| 406 | }, |
| 407 | |
| 408 | _alchemySVG() { |
| 409 | return ` |
| 410 | <svg class="alchemy-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true"> |
| 411 | <rect width="1400" height="900" fill="#020d08"/> |
| 412 | <g transform="translate(700, 450)"> |
| 413 | <circle class="sacred-geometry" cx="0" cy="0" r="350" /> |
| 414 | <circle class="sacred-geometry" cx="0" cy="0" r="220" /> |
| 415 | <circle class="sacred-geometry" cx="0" cy="0" r="100" /> |
| 416 | <polygon class="sacred-geometry" points="0,-350 303,175 -303,175" /> |
| 417 | <polygon class="sacred-geometry" points="0,350 -303,-175 303,-175" /> |
| 418 | <circle class="alchemical-ring" cx="0" cy="0" r="280" /> |
| 419 | </g> |
| 420 | </svg>`; |
| 421 | }, |
| 422 | |
| 423 | _minimalSVG() { |
| 424 | return ` |
| 425 | <svg class="minimal-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true"> |
| 426 | <rect width="1400" height="900" fill="#f8fafc"/> |
| 427 | <rect class="accent-block-blue" x="80" y="60" width="420" height="300" /> |
| 428 | <rect class="accent-block-red" x="940" y="420" width="360" height="330" /> |
| 429 | <g class="geo-line"> |
| 430 | <line x1="0" y1="200" x2="1400" y2="200" /> |
| 431 | <line x1="0" y1="600" x2="1400" y2="600" /> |
| 432 | <line x1="340" y1="0" x2="340" y2="900" /> |
| 433 | <line x1="1060" y1="0" x2="1060" y2="900" /> |
| 434 | </g> |
| 435 | </svg>`; |
| 436 | }, |
| 437 | |
| 438 | /** Capas de follaje del invernadero: el dosel de hojas se divide en tres |
| 439 | profundidades (atrás / medio / frente) que se mecen con el viento. */ |
| 440 | _greenhouseSVG() { |
| 441 | return ` |
| 442 | <div class="greenhouse-canopy"> |
| 443 | <svg class="canopy-svg canopy-layer-back leaf-sway" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> |
| 444 | <g fill="#A9C29B"> |
| 445 | <ellipse cx="90" cy="30" rx="60" ry="34" transform="rotate(30 90 30)"/> |
| 446 | <ellipse cx="180" cy="10" rx="50" ry="28" transform="rotate(-15 180 10)"/> |
| 447 | <ellipse cx="1300" cy="20" rx="65" ry="36" transform="rotate(-25 1300 20)"/> |
| 448 | <ellipse cx="1220" cy="50" rx="45" ry="26" transform="rotate(20 1220 50)"/> |
| 449 | <ellipse cx="70" cy="870" rx="70" ry="38" transform="rotate(-20 70 870)"/> |
| 450 | <ellipse cx="1330" cy="860" rx="60" ry="34" transform="rotate(15 1330 860)"/> |
| 451 | </g> |
| 452 | </svg> |
| 453 | <svg class="canopy-svg canopy-layer-mid leaf-sway-rev" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> |
| 454 | <g stroke="#3E6B4F" stroke-width="3" fill="none" stroke-linecap="round"> |
| 455 | <path d="M0,0 C40,60 10,140 60,200 C110,260 70,340 120,400"/> |
| 456 | <path d="M1400,0 C1360,70 1390,150 1330,210 C1270,270 1310,350 1250,410"/> |
| 457 | <path d="M0,900 C50,830 20,750 70,690 C120,630 80,560 130,500"/> |
| 458 | <path d="M1400,900 C1350,830 1380,750 1320,690 C1260,630 1300,560 1250,500"/> |
| 459 | </g> |
| 460 | <g fill="#5E8A63"> |
| 461 | <ellipse cx="60" cy="200" rx="20" ry="11" transform="rotate(35 60 200)"/> |
| 462 | <ellipse cx="120" cy="400" rx="18" ry="10" transform="rotate(-20 120 400)"/> |
| 463 | <ellipse cx="1330" cy="210" rx="20" ry="11" transform="rotate(-35 1330 210)"/> |
| 464 | <ellipse cx="1250" cy="410" rx="18" ry="10" transform="rotate(20 1250 410)"/> |
| 465 | <ellipse cx="70" cy="690" rx="19" ry="10" transform="rotate(-30 70 690)"/> |
| 466 | <ellipse cx="130" cy="500" rx="17" ry="9" transform="rotate(25 130 500)"/> |
| 467 | <ellipse cx="1320" cy="690" rx="19" ry="10" transform="rotate(30 1320 690)"/> |
| 468 | <ellipse cx="1250" cy="500" rx="17" ry="9" transform="rotate(-25 1250 500)"/> |
| 469 | </g> |
| 470 | </svg> |
| 471 | <svg class="canopy-svg canopy-layer-front leaf-drift" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> |
| 472 | <g fill="#3E6B4F"> |
| 473 | <ellipse cx="30" cy="0" rx="90" ry="55" transform="rotate(20 30 0)"/> |
| 474 | <ellipse cx="1370" cy="0" rx="95" ry="58" transform="rotate(-18 1370 0)"/> |
| 475 | <ellipse cx="10" cy="900" rx="85" ry="50" transform="rotate(-22 10 900)"/> |
| 476 | <ellipse cx="1390" cy="900" rx="88" ry="52" transform="rotate(18 1390 900)"/> |
| 477 | </g> |
| 478 | <g fill="#2F5340" opacity="0.5"> |
| 479 | <circle cx="120" cy="70" r="6"/> |
| 480 | <circle cx="1280" cy="80" r="5"/> |
| 481 | <circle cx="90" cy="830" r="5"/> |
| 482 | <circle cx="1310" cy="820" r="6"/> |
| 483 | </g> |
| 484 | </svg> |
| 485 | </div> |
| 486 | <div class="light-dapple"></div>`; |
| 487 | }, |
| 488 | |
| 489 | _limpiarFondo() { |
| 490 | document.querySelectorAll('.cyber-bg-viewport, .medieval-bg-viewport, .saiyan-bg-viewport, .noir-bg-viewport, .alchemy-bg-viewport, .minimal-bg-viewport, .invernadero-bg-viewport, .greenhouse-canopy, .light-dapple, .atmosphere-overlay, .castle-vignette, .cyber-blade-transition, .med-blade-transition, .saiyan-aura-transition, .noir-blade-transition, .alchemy-blade-transition, .minimal-blade-transition, .invernadero-leaf-transition, .heraldic-transition').forEach(el => el.remove()); |
| 491 | }, |
| 492 | |
| 493 | _limpiarAccentoInline() { |
| 494 | const r = document.documentElement; |
| 495 | for (const k of this._VARIABLES_ACCENTO) r.style.removeProperty(k); |
| 496 | }, |
| 497 | |
| 498 | /** Carga las fuentes decorativas (Cinzel/Bangers/Montserrat) cuando el |
| 499 | skin activo las usa; evita cargarlas en skins que no las necesitan. */ |
| 500 | _cargarFuentes() { |
| 501 | if (this._fuentesCargadas) return; |
| 502 | this._fuentesCargadas = true; |
| 503 | const id = 'estilos-fuentes'; |
| 504 | if (document.getElementById(id)) return; |
| 505 | const link = document.createElement('link'); |
| 506 | link.id = id; |
| 507 | link.rel = 'stylesheet'; |
| 508 | link.href = 'https://fonts.googleapis.com/css2?family=Bangers&family=Cinzel:wght@400;700;900&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@500;600;700;900&family=Share+Tech+Mono&family=Special+Elite&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Work+Sans:wght@400;500;600;700&display=swap'; |
| 509 | document.head.appendChild(link); |
| 510 | }, |
| 511 | |
| 512 | _ponFondo() { |
| 513 | const estilo = this.activo(); |
| 514 | if (estilo === 'medieval') return this._ponFondoMedieval(); |
| 515 | if (estilo === 'saiyan') return this._ponFondoSaiyan(); |
| 516 | if (estilo === 'noir') return this._ponFondoNoir(); |
| 517 | if (estilo === 'alchemy') return this._ponFondoAlchemy(); |
| 518 | if (estilo === 'minimal') return this._ponFondoMinimal(); |
| 519 | if (estilo === 'invernadero') return this._ponFondoInvernadero(); |
| 520 | return this._ponFondoCyber(); |
| 521 | }, |
| 522 | |
| 523 | _ponFondoCyber() { |
| 524 | if (document.querySelector('.cyber-bg-viewport')) return; |
| 525 | const inyectar = () => { |
| 526 | const viewport = document.createElement('div'); |
| 527 | viewport.className = 'cyber-bg-viewport'; |
| 528 | viewport.innerHTML = this._cyberSVG(); |
| 529 | |
| 530 | const atmosfera = document.createElement('div'); |
| 531 | atmosfera.className = 'atmosphere-overlay'; |
| 532 | |
| 533 | // Cortina de transición "acordeón neural" al navegar (mockup). |
| 534 | const blade = document.createElement('div'); |
| 535 | blade.className = 'cyber-blade-transition'; |
| 536 | blade.id = 'accordionBlade'; |
| 537 | blade.innerHTML = ` |
| 538 | <div class="accordion-panel"></div> |
| 539 | <div class="accordion-panel"></div> |
| 540 | <div class="accordion-panel"></div> |
| 541 | <div class="accordion-panel"></div>`; |
| 542 | |
| 543 | document.body.insertBefore(viewport, document.body.firstChild); |
| 544 | document.body.insertBefore(atmosfera, document.body.firstChild); |
| 545 | document.body.insertBefore(blade, document.body.firstChild); |
| 546 | this._registrarParallax(viewport.querySelector('.cyber-svg-layer'), 'cyber'); |
| 547 | this._bindBlade(); |
| 548 | }; |
| 549 | |
| 550 | if (document.body) { |
| 551 | inyectar(); |
| 552 | } else { |
| 553 | // El script corre en <head> (anti-FOUC): el fondo se inyecta al |
| 554 | // terminar de cargar el DOM, y de paso se vuelve a liberar el |
| 555 | // acento por si colores.js aplicó sus variables inline. |
| 556 | document.addEventListener('DOMContentLoaded', () => { |
| 557 | this._limpiarAccentoInline(); |
| 558 | inyectar(); |
| 559 | }, { once: true }); |
| 560 | } |
| 561 | }, |
| 562 | |
| 563 | _ponFondoMedieval() { |
| 564 | if (document.querySelector('.medieval-bg-viewport')) return; |
| 565 | this._cargarFuentes(); |
| 566 | const inyectar = () => { |
| 567 | const viewport = document.createElement('div'); |
| 568 | viewport.className = 'medieval-bg-viewport'; |
| 569 | viewport.innerHTML = this._castleSkyline(); |
| 570 | |
| 571 | // Viñeta nocturna del castillo. |
| 572 | const vineta = document.createElement('div'); |
| 573 | vineta.className = 'castle-vignette'; |
| 574 | |
| 575 | // Cortina heráldica "sello del castillo" al navegar (mockup El Castillo). |
| 576 | const heraldico = document.createElement('div'); |
| 577 | heraldico.className = 'heraldic-transition'; |
| 578 | heraldico.id = 'heraldicTransition'; |
| 579 | |
| 580 | document.body.insertBefore(viewport, document.body.firstChild); |
| 581 | document.body.insertBefore(vineta, document.body.firstChild); |
| 582 | document.body.insertBefore(heraldico, document.body.firstChild); |
| 583 | this._bindCastilloPan(); |
| 584 | this._bindBlade(); |
| 585 | }; |
| 586 | |
| 587 | if (document.body) { |
| 588 | inyectar(); |
| 589 | } else { |
| 590 | document.addEventListener('DOMContentLoaded', () => { |
| 591 | this._limpiarAccentoInline(); |
| 592 | inyectar(); |
| 593 | }, { once: true }); |
| 594 | } |
| 595 | }, |
| 596 | |
| 597 | _ponFondoSaiyan() { |
| 598 | if (document.querySelector('.saiyan-bg-viewport')) return; |
| 599 | this._cargarFuentes(); |
| 600 | const inyectar = () => { |
| 601 | const viewport = document.createElement('div'); |
| 602 | viewport.className = 'saiyan-bg-viewport'; |
| 603 | viewport.innerHTML = this._saiyanSVG(); |
| 604 | |
| 605 | const atmosfera = document.createElement('div'); |
| 606 | atmosfera.className = 'atmosphere-overlay'; |
| 607 | |
| 608 | // Cortina de transición "aura de ki" al navegar (mockup Saiyan). |
| 609 | const blade = document.createElement('div'); |
| 610 | blade.className = 'saiyan-aura-transition'; |
| 611 | |
| 612 | document.body.insertBefore(viewport, document.body.firstChild); |
| 613 | document.body.insertBefore(atmosfera, document.body.firstChild); |
| 614 | document.body.insertBefore(blade, document.body.firstChild); |
| 615 | this._registrarParallax(viewport.querySelector('.saiyan-svg-layer'), 'saiyan'); |
| 616 | this._bindBlade(); |
| 617 | }; |
| 618 | |
| 619 | if (document.body) { |
| 620 | inyectar(); |
| 621 | } else { |
| 622 | document.addEventListener('DOMContentLoaded', () => { |
| 623 | this._limpiarAccentoInline(); |
| 624 | inyectar(); |
| 625 | }, { once: true }); |
| 626 | } |
| 627 | }, |
| 628 | |
| 629 | _ponFondoNoir() { |
| 630 | if (document.querySelector('.noir-bg-viewport')) return; |
| 631 | this._cargarFuentes(); |
| 632 | const inyectar = () => { |
| 633 | const viewport = document.createElement('div'); |
| 634 | viewport.className = 'noir-bg-viewport'; |
| 635 | viewport.innerHTML = this._noirSVG(); |
| 636 | |
| 637 | const atmosfera = document.createElement('div'); |
| 638 | atmosfera.className = 'atmosphere-overlay'; |
| 639 | |
| 640 | // Cortina de transición "barrido" del callejón noir al navegar (mockup Noir). |
| 641 | const blade = document.createElement('div'); |
| 642 | blade.className = 'noir-blade-transition'; |
| 643 | |
| 644 | document.body.insertBefore(viewport, document.body.firstChild); |
| 645 | document.body.insertBefore(atmosfera, document.body.firstChild); |
| 646 | document.body.insertBefore(blade, document.body.firstChild); |
| 647 | this._registrarParallax(viewport.querySelector('.noir-svg-layer'), 'noir'); |
| 648 | this._bindBlade(); |
| 649 | }; |
| 650 | |
| 651 | if (document.body) { |
| 652 | inyectar(); |
| 653 | } else { |
| 654 | document.addEventListener('DOMContentLoaded', () => { |
| 655 | this._limpiarAccentoInline(); |
| 656 | inyectar(); |
| 657 | }, { once: true }); |
| 658 | } |
| 659 | }, |
| 660 | |
| 661 | _ponFondoAlchemy() { |
| 662 | if (document.querySelector('.alchemy-bg-viewport')) return; |
| 663 | this._cargarFuentes(); |
| 664 | const inyectar = () => { |
| 665 | const viewport = document.createElement('div'); |
| 666 | viewport.className = 'alchemy-bg-viewport'; |
| 667 | viewport.innerHTML = this._alchemySVG(); |
| 668 | |
| 669 | const atmosfera = document.createElement('div'); |
| 670 | atmosfera.className = 'atmosphere-overlay'; |
| 671 | |
| 672 | // Cortina de transmutación al navegar (mockup Alchemical Codex). |
| 673 | const blade = document.createElement('div'); |
| 674 | blade.className = 'alchemy-blade-transition'; |
| 675 | |
| 676 | document.body.insertBefore(viewport, document.body.firstChild); |
| 677 | document.body.insertBefore(atmosfera, document.body.firstChild); |
| 678 | document.body.insertBefore(blade, document.body.firstChild); |
| 679 | this._registrarParallax(viewport.querySelector('.alchemy-svg-layer'), 'alchemy'); |
| 680 | this._bindBlade(); |
| 681 | }; |
| 682 | |
| 683 | if (document.body) { |
| 684 | inyectar(); |
| 685 | } else { |
| 686 | document.addEventListener('DOMContentLoaded', () => { |
| 687 | this._limpiarAccentoInline(); |
| 688 | inyectar(); |
| 689 | }, { once: true }); |
| 690 | } |
| 691 | }, |
| 692 | |
| 693 | _ponFondoMinimal() { |
| 694 | if (document.querySelector('.minimal-bg-viewport')) return; |
| 695 | this._cargarFuentes(); |
| 696 | const inyectar = () => { |
| 697 | const viewport = document.createElement('div'); |
| 698 | viewport.className = 'minimal-bg-viewport'; |
| 699 | viewport.innerHTML = this._minimalSVG(); |
| 700 | |
| 701 | // Cortina tricolor (azul→blanco→rojo) al navegar (mockup Swiss). |
| 702 | const blade = document.createElement('div'); |
| 703 | blade.className = 'minimal-blade-transition'; |
| 704 | |
| 705 | document.body.insertBefore(viewport, document.body.firstChild); |
| 706 | document.body.insertBefore(blade, document.body.firstChild); |
| 707 | this._registrarParallax(viewport.querySelector('.minimal-svg-layer'), 'minimal'); |
| 708 | this._bindBlade(); |
| 709 | }; |
| 710 | |
| 711 | if (document.body) { |
| 712 | inyectar(); |
| 713 | } else { |
| 714 | document.addEventListener('DOMContentLoaded', () => { |
| 715 | this._limpiarAccentoInline(); |
| 716 | inyectar(); |
| 717 | }, { once: true }); |
| 718 | } |
| 719 | }, |
| 720 | |
| 721 | _ponFondoInvernadero() { |
| 722 | if (document.querySelector('.invernadero-bg-viewport')) return; |
| 723 | this._cargarFuentes(); |
| 724 | const inyectar = () => { |
| 725 | const viewport = document.createElement('div'); |
| 726 | viewport.className = 'invernadero-bg-viewport'; |
| 727 | viewport.innerHTML = this._greenhouseSVG(); |
| 728 | |
| 729 | // Cortina de transición "hoja" al navegar (mockup invernadero). |
| 730 | const leaf = document.createElement('div'); |
| 731 | leaf.className = 'invernadero-leaf-transition'; |
| 732 | leaf.id = 'leafTransition'; |
| 733 | |
| 734 | document.body.insertBefore(viewport, document.body.firstChild); |
| 735 | document.body.insertBefore(leaf, document.body.firstChild); |
| 736 | this._bindBlade(); |
| 737 | }; |
| 738 | |
| 739 | if (document.body) { |
| 740 | inyectar(); |
| 741 | } else { |
| 742 | document.addEventListener('DOMContentLoaded', () => { |
| 743 | this._limpiarAccentoInline(); |
| 744 | inyectar(); |
| 745 | }, { once: true }); |
| 746 | } |
| 747 | }, |
| 748 | |
| 749 | /** |
| 750 | * Motor de parallax unificado: un único listener de mousemove comparte el |
| 751 | * cursor y una única RAF por frame aplica el transform al fondo del skin |
| 752 | * activo. En vez de 6 listeners y una escritura de estilo por evento, |
| 753 | * hay 1 listener y como máximo 1 write por frame (lo esencial para |
| 754 | * suavidad en pantallas de 60-120 Hz). |
| 755 | */ |
| 756 | _registrarParallax(layer, color, extra) { |
| 757 | if (!layer || !color || layer.dataset.parallax) return; |
| 758 | layer.dataset.parallax = '1'; |
| 759 | if (!this._parallaxCapas) this._parallaxCapas = {}; |
| 760 | this._parallaxCapas[color] = { layer, extra: extra || null }; |
| 761 | if (this._parallaxListo) return; |
| 762 | this._parallaxListo = true; |
| 763 | window.addEventListener('mousemove', e => { |
| 764 | this._parallaxX = e.clientX; |
| 765 | this._parallaxY = e.clientY; |
| 766 | if (this._parallaxRAFId) return; |
| 767 | this._parallaxRAFId = requestAnimationFrame(() => { |
| 768 | this._parallaxRAFId = 0; |
| 769 | this._aplicarParallax(); |
| 770 | }); |
| 771 | }, { passive: true }); |
| 772 | }, |
| 773 | |
| 774 | /** Aplica el parallax del skin activo con su geometría específica. */ |
| 775 | _aplicarParallax() { |
| 776 | const estilo = document.documentElement.getAttribute('data-estilo'); |
| 777 | const capa = this._parallaxCapas && this._parallaxCapas[estilo]; |
| 778 | if (!capa) return; |
| 779 | const layer = capa.layer; |
| 780 | // Quita la transición CSS en el primer movimiento para que el |
| 781 | // parallax siga el cursor de forma inmediata y no se sienta "muerto". |
| 782 | if (layer.style.transition !== 'none') layer.style.transition = 'none'; |
| 783 | const x = (this._parallaxX / window.innerWidth) - 0.5; |
| 784 | const y = (this._parallaxY / window.innerHeight) - 0.5; |
| 785 | let t = ''; |
| 786 | switch (estilo) { |
| 787 | case 'cyber': |
| 788 | t = `translate3d(${x * 30}px, ${y * 22}px, 0) scale(1.05) rotateX(${15 - y * 8}deg)`; |
| 789 | break; |
| 790 | case 'medieval': |
| 791 | t = `translate3d(${x * 24}px, ${y * 18}px, 0) scale(${1 + y * 0.05}) rotateY(${x * 4}deg)`; |
| 792 | if (capa.extra) capa.extra.style.transform = `translateX(${x * -35}px) translateY(${y * -22}px)`; |
| 793 | break; |
| 794 | case 'saiyan': |
| 795 | t = `translate3d(${x * -30}px, ${y * -20}px, 0) scale(${1 + y * 0.06})`; |
| 796 | break; |
| 797 | case 'noir': |
| 798 | t = `translate3d(${x * 22}px, ${y * 16}px, 0) scale(${1 + y * 0.05}) rotateX(${8 - y * 5}deg)`; |
| 799 | break; |
| 800 | case 'alchemy': |
| 801 | t = `translate3d(${x * 30}px, ${y * 22}px, 0) scale(${1 + y * 0.05}) rotateX(${15 - y * 8}deg)`; |
| 802 | break; |
| 803 | case 'minimal': |
| 804 | t = `translate3d(${x * 18}px, ${y * 14}px, 0) scale(${1 + y * 0.03}) rotateX(${4 - y * 2}deg)`; |
| 805 | break; |
| 806 | } |
| 807 | if (t) layer.style.transform = t; |
| 808 | }, |
| 809 | |
| 810 | /** Cortina de transición al navegar entre páginas del sidebar. */ |
| 811 | _bindBlade() { |
| 812 | if (this._bladeBinded) return; |
| 813 | this._bladeBinded = true; |
| 814 | |
| 815 | document.addEventListener('click', (e) => { |
| 816 | const estilo = document.documentElement.getAttribute('data-estilo'); |
| 817 | if (estilo !== 'cyber' && estilo !== 'medieval' && estilo !== 'saiyan' && estilo !== 'noir' && estilo !== 'alchemy' && estilo !== 'minimal' && estilo !== 'invernadero') return; |
| 818 | if (!(e.target instanceof Element)) return; |
| 819 | |
| 820 | const enlace = e.target.closest('#sidebar a[href$=".html"], .sidebar-nav a[href$=".html"]'); |
| 821 | if (!enlace) return; |
| 822 | if (e.defaultPrevented) return; |
| 823 | if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (e.button !== undefined && e.button !== 0)) return; |
| 824 | |
| 825 | const href = enlace.getAttribute('href'); |
| 826 | if (!href || href === '#') return; |
| 827 | |
| 828 | if (estilo === 'medieval') { |
| 829 | // Transición heráldica del sello del castillo (mockup El Castillo). |
| 830 | const heraldico = document.querySelector('.heraldic-transition'); |
| 831 | if (!heraldico) return; |
| 832 | e.preventDefault(); |
| 833 | this._triggerHeraldico(() => { window.location.href = href; }); |
| 834 | return; |
| 835 | } |
| 836 | |
| 837 | if (estilo === 'cyber') { |
| 838 | // Transición de "acordeón neural": cierra los pliegues, cambia la |
| 839 | // vista en el punto medio y luego abre los pliegues nuevamente. |
| 840 | const accordion = document.querySelector('.cyber-blade-transition'); |
| 841 | if (!accordion) return; |
| 842 | e.preventDefault(); |
| 843 | this._triggerAccordion(() => { window.location.href = href; }); |
| 844 | return; |
| 845 | } |
| 846 | |
| 847 | if (estilo === 'invernadero') { |
| 848 | // Cortina de círculo "hoja creciendo" (mockup invernadero). |
| 849 | const leaf = document.querySelector('.invernadero-leaf-transition'); |
| 850 | if (!leaf) return; |
| 851 | e.preventDefault(); |
| 852 | this._triggerLeaf(() => { window.location.href = href; }); |
| 853 | return; |
| 854 | } |
| 855 | |
| 856 | const blade = document.querySelector(estilo === 'noir' ? '.noir-blade-transition' : estilo === 'alchemy' ? '.alchemy-blade-transition' : estilo === 'minimal' ? '.minimal-blade-transition' : '.saiyan-aura-transition'); |
| 857 | if (!blade) return; |
| 858 | |
| 859 | e.preventDefault(); |
| 860 | blade.classList.add('active'); |
| 861 | setTimeout(() => { window.location.href = href; }, estilo === 'noir' ? 340 : estilo === 'alchemy' ? 340 : estilo === 'minimal' ? 350 : 350); |
| 862 | }, true); |
| 863 | }, |
| 864 | |
| 865 | /** Dispara la cortina de acordeón: cierra los pliegues, ejecuta onComplete |
| 866 | en el punto medio (400 ms) y abre los pliegues nuevamente. */ |
| 867 | _triggerAccordion(onComplete) { |
| 868 | const accordion = document.querySelector('.cyber-blade-transition'); |
| 869 | if (!accordion) { |
| 870 | if (typeof onComplete === 'function') onComplete(); |
| 871 | return; |
| 872 | } |
| 873 | |
| 874 | accordion.classList.add('active'); |
| 875 | |
| 876 | setTimeout(() => { |
| 877 | if (typeof onComplete === 'function') onComplete(); |
| 878 | accordion.classList.remove('active'); |
| 879 | }, 400); |
| 880 | }, |
| 881 | |
| 882 | /** Cortina de círculo "hoja creciendo" del invernadero: se abre un círculo |
| 883 | de follaje, se cambia la vista en el pico y se cierra de nuevo. */ |
| 884 | _triggerLeaf(onComplete) { |
| 885 | const leaf = document.querySelector('.invernadero-leaf-transition'); |
| 886 | if (!leaf) { |
| 887 | if (typeof onComplete === 'function') onComplete(); |
| 888 | return; |
| 889 | } |
| 890 | leaf.classList.add('active'); |
| 891 | setTimeout(() => { |
| 892 | if (typeof onComplete === 'function') onComplete(); |
| 893 | leaf.classList.remove('active'); |
| 894 | }, 320); |
| 895 | }, |
| 896 | |
| 897 | /** Dispara la cortina heráldica del sello: se abre un círculo desde un |
| 898 | vértice superior y ejecuta onComplete en el punto medio, luego se limpia. */ |
| 899 | _triggerHeraldico(onComplete) { |
| 900 | const container = document.querySelector('.heraldic-transition'); |
| 901 | if (!container) { |
| 902 | if (typeof onComplete === 'function') onComplete(); |
| 903 | return; |
| 904 | } |
| 905 | |
| 906 | container.classList.add('active'); |
| 907 | this._panCastilloZona(); |
| 908 | |
| 909 | setTimeout(() => { |
| 910 | if (typeof onComplete === 'function') onComplete(); |
| 911 | }, 260); |
| 912 | |
| 913 | setTimeout(() => { |
| 914 | container.classList.remove('active'); |
| 915 | }, 620); |
| 916 | }, |
| 917 | |
| 918 | /* ------------------------------------------------------------ |
| 919 | Aplicación |
| 920 | ------------------------------------------------------------ */ |
| 921 | |
| 922 | /** Aplica un estilo visual y (por defecto) lo guarda como preferencia. */ |
| 923 | set(id, guardar = true) { |
| 924 | const estilo = this.STYLES.some(s => s.id === id) ? id : 'clasico'; |
| 925 | const r = document.documentElement; |
| 926 | r.setAttribute('data-estilo', estilo); |
| 927 | |
| 928 | if (estilo === 'clasico') { |
| 929 | this._limpiarFondo(); |
| 930 | // El color de acento vuelve a manos de la personalización (js/colores.js). |
| 931 | if (typeof Colores !== 'undefined') Colores.aplicar(); |
| 932 | } else { |
| 933 | // El estilo define su propia paleta: libera las variables inline de |
| 934 | // la personalización para que las del skin (estilos.css) manden. |
| 935 | this._limpiarAccentoInline(); |
| 936 | this._ponFondo(); |
| 937 | } |
| 938 | |
| 939 | if (guardar) { |
| 940 | try { localStorage.setItem(this.KEY, estilo); } catch (e) { /* almacenamiento no disponible */ } |
| 941 | } |
| 942 | document.dispatchEvent(new CustomEvent('estilos:cambio', { detail: { estilo } })); |
| 943 | this._aplicarTextos(); |
| 944 | if (guardar) this._mostrarRecarga(); |
| 945 | return estilo; |
| 946 | }, |
| 947 | |
| 948 | /** Aplica los textos temáticos del skin activo sobre botones, enlaces y |
| 949 | placeholders (coincidencia exacta e idempotente). */ |
| 950 | _aplicarTextos() { |
| 951 | const mapa = this.TEXTOS_ESTILO[this.activo()]; |
| 952 | if (!mapa || !document.body) return; |
| 953 | const normalizar = t => (t || '').replace(/\s+/g, ' ').trim(); |
| 954 | document.querySelectorAll('button, a').forEach(el => { |
| 955 | const txt = normalizar(el.textContent); |
| 956 | if (mapa[txt]) el.textContent = mapa[txt]; |
| 957 | }); |
| 958 | document.querySelectorAll('input, textarea').forEach(el => { |
| 959 | const ph = el.getAttribute('placeholder'); |
| 960 | if (ph && mapa[ph]) el.setAttribute('placeholder', mapa[ph]); |
| 961 | }); |
| 962 | }, |
| 963 | |
| 964 | /** Aviso con botón de recarga al cambiar de estilo en caliente: evita que |
| 965 | algunos elementos se queden con aspecto raro hasta el próximo full-load. */ |
| 966 | _mostrarRecarga() { |
| 967 | if (document.querySelector('.estilos-recarga-overlay')) return; |
| 968 | if (!document.body) { |
| 969 | document.addEventListener('DOMContentLoaded', () => this._mostrarRecarga(), { once: true }); |
| 970 | return; |
| 971 | } |
| 972 | const acento = { clasico: '#3fb950', cyber: '#00f0ff', medieval: '#A9834B', saiyan: '#facc15', noir: '#e11d48', alchemy: '#10b981', minimal: '#2563eb', invernadero: '#3E6B4F' }[this.activo()] || '#3fb950'; |
| 973 | const ov = document.createElement('div'); |
| 974 | ov.className = 'estilos-recarga-overlay'; |
| 975 | ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.72);backdrop-filter:blur(6px);z-index:999999;display:flex;align-items:center;justify-content:center;padding:1rem;overflow:auto;'; |
| 976 | ov.innerHTML = ` |
| 977 | <div class="estilos-recarga-card" style="background:#161b22;border:2px solid ${acento};box-shadow:0 0 45px rgba(0,0,0,0.7);padding:30px 34px;max-width:430px;width:100%;max-height:calc(100vh - 2rem);overflow:auto;text-align:center;flex-shrink:0;margin:auto;"> |
| 978 | <div style="font-size:2.2rem;line-height:1;">⚡</div> |
| 979 | <h3 style="margin:12px 0 8px;font-size:1.3rem;font-weight:900;color:#ffffff;">Estilo aplicado</h3> |
| 980 | <p style="margin:0 0 20px;font-size:0.92rem;color:#8b949e;line-height:1.5;">Para que el nuevo estilo se aplique por completo y no se vea raro, recarga la página.</p> |
| 981 | <div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;"> |
| 982 | <button type="button" class="est-recargar-btn" style="background:${acento};color:#0b1220;border:none;padding:11px 22px;font-weight:800;font-size:0.95rem;cursor:pointer;">Recargar ahora</button> |
| 983 | <button type="button" class="est-quedarme-btn" style="background:transparent;color:#c9d1d9;border:1px solid #30363d;padding:10px 18px;font-weight:600;font-size:0.9rem;cursor:pointer;">Quedarme aquí</button> |
| 984 | </div> |
| 985 | </div>`; |
| 986 | document.body.appendChild(ov); |
| 987 | |
| 988 | const cerrar = () => { ov.remove(); }; |
| 989 | ov.querySelector('.est-recargar-btn').addEventListener('click', () => window.location.reload(), { once: true }); |
| 990 | ov.querySelector('.est-quedarme-btn').addEventListener('click', cerrar, { once: true }); |
| 991 | document.addEventListener('keydown', e => { if (e.key === 'Escape') cerrar(); }, { once: true }); |
| 992 | }, |
| 993 | |
| 994 | /** Re-aplica los textos cuando se re-renderiza contenido dinámico |
| 995 | (posts, modales, resultados de búsqueda...). */ |
| 996 | _vigilarTextos() { |
| 997 | if (this._texVigila) return; |
| 998 | this._texVigila = true; |
| 999 | this._aplicarTextos(); |
| 1000 | if (!('MutationObserver' in window)) return; |
| 1001 | if (!this.TEXTOS_ESTILO[this.activo()]) return; |
| 1002 | let timer = null; |
| 1003 | new MutationObserver(() => { |
| 1004 | clearTimeout(timer); |
| 1005 | timer = setTimeout(() => this._aplicarTextos(), 350); |
| 1006 | }).observe(document.body, { childList: true, subtree: true }); |
| 1007 | }, |
| 1008 | |
| 1009 | /** Aplica el estilo guardado al cargar la página (sin volver a guardar). */ |
| 1010 | init() { |
| 1011 | this.set(this.guardado(), false); |
| 1012 | if (document.body) { |
| 1013 | this._vigilarTextos(); |
| 1014 | } else { |
| 1015 | document.addEventListener('DOMContentLoaded', () => this._vigilarTextos(), { once: true }); |
| 1016 | } |
| 1017 | } |
| 1018 | }; |
| 1019 | |
| 1020 | /* Se aplica en cuanto se carga el script, igual que Tema y Colores (evita parpadeo). */ |
| 1021 | Estilos.init(); |