1/** 2 * main.js 3 * Main JavaScript functionality for The Regenerates website 4 */ 5 6// Wait for DOM content to be fully loaded 7document.addEventListener('DOMContentLoaded', function() { 8 // Initialize all components 9 initNavigation(); 10 initCharacterCards(); 11 initModal(); 12 initChapterTabs(); 13 initScrollAnimations(); 14 initParticles(); 15 16 // Show page after everything is loaded 17 window.addEventListener('load', function() { 18 document.body.classList.add('loaded'); 19 document.getElementById('loadingOverlay').classList.remove('active'); 20 }); 21}); 22 23/** 24 * Initialize responsive navigation 25 */ 26function initNavigation() { 27 const navbar = document.getElementById('mainNav'); 28 const navToggle = document.querySelector('.mobile-menu-toggle'); 29 const navLinks = document.getElementById('navLinks'); 30 31 // Mobile menu toggle 32 if (navToggle && navLinks) { 33 navToggle.addEventListener('click', function() { 34 navToggle.classList.toggle('active'); 35 navLinks.classList.toggle('active'); 36 37 // Update ARIA attributes 38 const expanded = navToggle.classList.contains('active'); 39 navToggle.setAttribute('aria-expanded', expanded); 40 41 // Prevent scrolling when menu is open 42 document.body.style.overflow = expanded ? 'hidden' : ''; 43 }); 44 45 // Close menu when link is clicked 46 const links = navLinks.querySelectorAll('a'); 47 links.forEach(link => { 48 link.addEventListener('click', function() { 49 navToggle.classList.remove('active'); 50 navLinks.classList.remove('active'); 51 navToggle.setAttribute('aria-expanded', false); 52 document.body.style.overflow = ''; 53 }); 54 }); 55 } 56 57 // Compact navbar on scroll 58 window.addEventListener('scroll', function() { 59 if (window.scrollY > 100) { 60 navbar.classList.add('compact'); 61 } else { 62 navbar.classList.remove('compact'); 63 } 64 }); 65 66 // Close menu on escape key 67 document.addEventListener('keydown', function(event) { 68 if (event.key === 'Escape' && navLinks.classList.contains('active')) { 69 navToggle.classList.remove('active'); 70 navLinks.classList.remove('active'); 71 navToggle.setAttribute('aria-expanded', false); 72 document.body.style.overflow = ''; 73 } 74 }); 75} 76 77/** 78 * Initialize character cards with proper animations 79 */ 80function initCharacterCards() { 81 const powerFills = document.querySelectorAll('.power-fill'); 82 83 // Animate power meters on load 84 powerFills.forEach(fill => { 85 // Store original width 86 const width = fill.style.width; 87 fill.style.width = '0'; 88 89 // Use IntersectionObserver to only animate when visible 90 const observer = new IntersectionObserver((entries) => { 91 entries.forEach(entry => { 92 if (entry.isIntersecting) { 93 setTimeout(() => { 94 fill.style.width = width; 95 }, 300); 96 observer.unobserve(entry.target); 97 } 98 }); 99 }, { threshold: 0.3 }); 100 101 observer.observe(fill); 102 }); 103 104 // Add hover effects to power meters 105 const characterCards = document.querySelectorAll('.character-card'); 106 characterCards.forEach(card => { 107 card.addEventListener('mouseenter', function() { 108 const powerFill = this.querySelector('.power-fill'); 109 if (powerFill) { 110 powerFill.style.boxShadow = '0 0 10px rgba(98, 126, 234, 0.7)'; 111 } 112 }); 113 114 card.addEventListener('mouseleave', function() { 115 const powerFill = this.querySelector('.power-fill'); 116 if (powerFill) { 117 powerFill.style.boxShadow = 'none'; 118 } 119 }); 120 121 // Add keyboard navigation support 122 card.setAttribute('tabindex', '0'); 123 card.addEventListener('keydown', function(event) { 124 if (event.key === 'Enter' || event.key === ' ') { 125 event.preventDefault(); 126 this.click(); 127 } 128 }); 129 }); 130} 131 132/** 133 * Initialize modal functionality 134 */ 135function initModal() { 136 const modal = document.getElementById('characterModal'); 137 const characterCards = document.querySelectorAll('.character-card'); 138 const closeBtn = document.querySelector('.modal .close'); 139 const closeModalBtn = document.getElementById('closeModal'); 140 141 if (!modal || !characterCards.length) return; 142 143 // Open modal function 144 function openModal(characterId) { 145 if (!characterData[characterId]) { 146 console.error('Character data not found for:', characterId); 147 return; 148 } 149 150 const character = characterData[characterId]; 151 152 // Set modal content 153 document.getElementById('modalName').textContent = character.name; 154 document.getElementById('modalRole').textContent = character.role; 155 document.getElementById('modalImage').style.backgroundImage = `url(${character.image})`; 156 document.getElementById('modalOrigin').textContent = character.origin; 157 document.getElementById('modalBirthplace').textContent = character.birthplace; 158 document.getElementById('modalAge').textContent = character.age; 159 document.getElementById('modalPower').textContent = character.power; 160 161 // Set powers list 162 const powersList = document.getElementById('modalPowers'); 163 powersList.innerHTML = '';
164 character.powers.forEach(power => { 165 const li = document.createElement('li'); 166 li.textContent = power; 167 powersList.appendChild(li); 168 }); 169 170 // Set story 171 document.getElementById('modalStory').textContent = character.story; 172 173 // Show modal 174 modal.style.display = 'block'; 175 modal.setAttribute('aria-hidden', 'false'); 176 document.body.style.overflow = 'hidden'; 177 178 // Focus on close button for keyboard accessibility 179 closeBtn.focus(); 180 } 181 182 // Close modal function 183 function closeModal() { 184 modal.style.display = 'none'; 185 modal.setAttribute('aria-hidden', 'true'); 186 document.body.style.overflow = 'auto'; 187 } 188 189 // Add click event to character cards 190 characterCards.forEach(card => { 191 card.addEventListener('click', function() { 192 const characterId = this.getAttribute('data-character'); 193 openModal(characterId); 194 }); 195 }); 196 197 // Close modal events 198 if (closeBtn) { 199 closeBtn.addEventListener('click', closeModal); 200 } 201 202 if (closeModalBtn) { 203 closeModalBtn.addEventListener('click', closeModal); 204 } 205 206 // Close modal when clicking outside 207 window.addEventListener('click', function(event) { 208 if (event.target === modal) { 209 closeModal(); 210 } 211 }); 212 213 // Close modal with Escape key 214 document.addEventListener('keydown', function(event) { 215 if (event.key === 'Escape' && modal.style.display === 'block') { 216 closeModal(); 217 } 218 }); 219} 220 221/** 222 * Initialize chapter tabs functionality 223 */ 224function initChapterTabs() { 225 const chapterTabs = document.querySelectorAll('.chapter-tab'); 226 const chapterContents = document.querySelectorAll('.chapter-content'); 227 228 if (!chapterTabs.length || !chapterContents.length) return; 229 230 chapterTabs.forEach(tab => { 231 tab.addEventListener('click', function() { 232 const chapter = this.getAttribute('data-chapter'); 233 234 // Update tab state 235 chapterTabs.forEach(t => { 236 t.classList.remove('active'); 237 t.setAttribute('aria-selected', 'false'); 238 }); 239 240 this.classList.add('active'); 241 this.setAttribute('aria-selected', 'true'); 242 243 // Update content state 244 chapterContents.forEach(c => c.classList.remove('active')); 245 246 const targetContent = document.getElementById(`chapter-${chapter}`); 247 if (targetContent) { 248 targetContent.classList.add('active'); 249 } 250 }); 251 252 // Add keyboard handling 253 tab.addEventListener('keydown', function(event) { 254 let index = Array.from(chapterTabs).indexOf(this); 255 let newIndex; 256 257 if (event.key === 'ArrowRight') { 258 newIndex = (index + 1) % chapterTabs.length; 259 chapterTabs[newIndex].click(); 260 chapterTabs[newIndex].focus(); 261 event.preventDefault(); 262 } else if (event.key === 'ArrowLeft') { 263 newIndex = (index - 1 + chapterTabs.length) % chapterTabs.length; 264 chapterTabs[newIndex].click(); 265 chapterTabs[newIndex].focus(); 266 event.preventDefault(); 267 } 268 }); 269 }); 270} 271 272/** 273 * Initialize scroll animations 274 */ 275function initScrollAnimations() { 276 const elements = document.querySelectorAll('.character-card, .location-card'); 277 278 if (!elements.length) return; 279 280 // Check for IntersectionObserver support 281 if ('IntersectionObserver' in window) { 282 const observer = new IntersectionObserver((entries) => { 283 entries.forEach((entry, index) => { 284 if (entry.isIntersecting) { 285 // Add staggered animation delay 286 setTimeout(() => { 287 entry.target.classList.add('animate-in'); 288 }, index * 100); 289 290 // Stop observing after animation 291 observer.unobserve(entry.target); 292 } 293 }); 294 }, { threshold: 0.2 }); 295
296 elements.forEach(element => { 297 // Set initial styles 298 element.style.opacity = '0'; 299 element.style.transform = 'translateY(50px)'; 300 element.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; 301 302 // Add a class that will be targeted when visible 303 element.classList.add('scroll-animate'); 304 305 // Start observing 306 observer.observe(element); 307 }); 308 309 // Add CSS for the animation 310 const style = document.createElement('style'); 311 style.textContent = ` 312 .scroll-animate.animate-in { 313 opacity: 1 !important; 314 transform: translateY(0) !important; 315 } 316 `; 317 document.head.appendChild(style); 318 } else { 319 // Fallback for browsers without IntersectionObserver 320 elements.forEach(element => { 321 element.style.opacity = '1'; 322 element.style.transform = 'translateY(0)'; 323 }); 324 } 325} 326 327/** 328 * Initialize particles.js effect with fallback 329 */ 330function initParticles() { 331 const particlesContainer = document.getElementById('particles-js'); 332 333 if (!particlesContainer) return; 334 335 // Function to initialize particles 336 function setupParticles() { 337 if (typeof particlesJS !== 'undefined') { 338 particlesJS('particles-js', { 339 particles: { 340 number: { value: 80, density: { enable: true, value_area: 800 } }, 341 color: { value: "#87CEEB" }, 342 shape: { type: "circle" }, 343 opacity: { value: 0.5, random: true }, 344 size: { value: 3, random: true }, 345 line_linked: { 346 enable: true, 347 distance: 150, 348 color: "#4682B4", 349 opacity: 0.4, 350 width: 1 351 }, 352 move: { 353 enable: true, 354 speed: 2, 355 direction: "none", 356 random: true, 357 straight: false, 358 out_mode: "out", 359 bounce: false 360 } 361 }, 362 interactivity: { 363 detect_on: "canvas", 364 events: { 365 onhover: { enable: true, mode: "grab" }, 366 onclick: { enable: true, mode: "push" }, 367 resize: true 368 }, 369 modes: { 370 grab: { distance: 140, line_linked: { opacity: 1 } }, 371 push: { particles_nb: 4 } 372 } 373 }, 374 retina_detect: true 375 }); 376 } else { 377 console.warn("Particles.js not loaded, using fallback"); 378 createParticlesFallback(particlesContainer); 379 } 380 } 381 382 // Fallback floating elements if particles.js fails to load 383 function createParticlesFallback(container) { 384 // Add CSS for fallback particles 385 const style = document.createElement('style'); 386 style.textContent = ` 387 .fallback-particle { 388 position: absolute; 389 background-color: rgba(135, 206, 235, 0.5); 390 border-radius: 50%; 391 pointer-events: none; 392 } 393 @keyframes float { 394 0% { transform: translate(0, 0) rotate(0deg); } 395 25% { transform: translate(10px, 10px) rotate(5deg); } 396 50% { transform: translate(0, 20px) rotate(0deg); } 397 75% { transform: translate(-10px, 10px) rotate(-5deg); } 398 100% { transform: translate(0, 0) rotate(0deg); } 399 } 400 `; 401 document.head.appendChild(style); 402 403 // Create fallback particles 404 for (let i = 0; i < 20; i++) { 405 const particle = document.createElement('div'); 406 particle.className = 'fallback-particle'; 407 408 // Random size 409 const size = Math.random() * 8 + 2; 410 particle.style.width = size + 'px'; 411 particle.style.height = size + 'px'; 412 413 // Random position 414 particle.style.left = Math.random() * 100 + '%'; 415 particle.style.top = Math.random() * 100 + '%'; 416 417 // Animation 418 const duration = Math.random() * 10 + 10; 419 particle.style.animation = `float ${duration}s infinite ease-in-out`; 420 particle.style.animationDelay = Math.random() * 5 + 's'; 421 422 container.appendChild(particle); 423 } 424 } 425 426 // Try to load particles.js 427 if (typeof particlesJS === 'undefined') { 428 const script = document.createElement('script'); 429 script.src = 'https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js'; 430 script.async = true; 431 432 // Set timeout for script loading 433 const timeout = setTimeout(() => { 434 console.warn("Particles.js load timeout, using fallback"); 435 createParticlesFallback(particlesContainer); 436 }, 5000); 437 438 script.onload = () => { 439 clearTimeout(timeout); 440 setupParticles(); 441 }; 442 443 script.onerror = () => { 444 clearTimeout(timeout); 445 console.warn("Failed to load particles.js, using fallback"); 446 createParticlesFallback(particlesContainer); 447 }; 448 449 document.head.appendChild(script); 450 } else { 451 setupParticles(); 452 } 453} 454 455/** 456 * Check if element is in viewport 457 * @param {Element} element - DOM element to check 458 * @returns {boolean} - True if element is in viewport 459 */ 460function isInViewport(element) { 461 const rect = element.getBoundingClientRect(); 462 return ( 463 rect.top <= (window.innerHeight || document.documentElement.clientHeight) && 464 rect.bottom >= 0 && 465 rect.left <= (window.innerWidth || document.documentElement.clientWidth) && 466 rect.right >= 0 467 ); 468} 469 470/** 471 * Debounce function to limit how often a function can be called 472 * @param {Function} func - Function to debounce 473 * @param {number} wait - Wait time in milliseconds 474 * @returns {Function} - Debounced function 475 */ 476function debounce(func, wait) { 477 let timeout; 478 return function() { 479 const context = this; 480 const args = arguments; 481 clearTimeout(timeout); 482 timeout = setTimeout(() => {
483 func.apply(context, args); 484 }, wait); 485 }; 486}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.