PageSourceSearch

https://captainethereum.info/regenerates/js/main.js

js captainethereum.info collected 2026-10-08 10:51:06 UTC 16,728 bytes, 486 lines download raw bytes

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.