Diff of /web/index.js [000000] .. [8c4ad8]

Switch to unified view

a b/web/index.js
1
/**
2
 * DNAnalyzer Main JavaScript
3
 * Handles animations, interactivity and UI functionality
4
 */
5
6
document.addEventListener('DOMContentLoaded', function() {
7
    // Initialize DNA Helix animation
8
    initDNAHelix();
9
    
10
    // Initialize mobile menu toggle
11
    initMobileMenu();
12
    
13
    // Initialize navbar scroll effect
14
    initNavbarScroll();
15
    
16
    // Initialize stats counter animation
17
    initStatsAnimation();
18
    
19
    // Initialize notification banner dismiss functionality
20
    initNotificationBanner();
21
22
    // Initialize notification banner scroll behavior
23
    initNotificationScroll();
24
    
25
    // Initialize navbar pulse effect
26
    initNavbarPulse();
27
});
28
29
// Shared state for notification banner
30
let notificationClosed = false;
31
32
/**
33
 * Initialize the notification banner dismiss functionality and adjust navbar positioning
34
 */
35
function initNotificationBanner() {
36
    const banner = document.querySelector('.notification-banner');
37
    const navbar = document.getElementById('navbar');
38
    if (!banner || !navbar) return;
39
40
    const bannerHeight = banner.offsetHeight;
41
    document.documentElement.style.setProperty('--notification-height', `${bannerHeight}px`);
42
43
    const closeBtn = document.querySelector('.notification-banner .notification-close');
44
    if (!closeBtn) return;
45
46
    closeBtn.addEventListener('click', function() {
47
        banner.classList.add('closed');
48
        document.documentElement.style.setProperty('--notification-height', '0px');
49
        navbar.style.top = '0';
50
        notificationClosed = true;
51
    });
52
53
    // Initial positioning
54
    navbar.style.top = `${bannerHeight}px`;
55
}
56
57
/**
58
 * Initialize notification banner scroll behavior
59
 */
60
function initNotificationScroll() {
61
    const banner = document.querySelector('.notification-banner');
62
    const navbar = document.getElementById('navbar');
63
    if (!banner || !navbar) return;
64
65
    let lastScrollTop = 0;
66
    const bannerHeight = banner.offsetHeight;
67
68
    window.addEventListener('scroll', function() {
69
        let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
70
        if (scrollTop > lastScrollTop && scrollTop > bannerHeight) {
71
            // Scrolling down
72
            banner.classList.add('hide-notification');
73
            navbar.style.top = '0';
74
        } else if (!notificationClosed) {
75
            // Scrolling up and notification was not manually closed
76
            banner.classList.remove('hide-notification');
77
            navbar.style.top = `${bannerHeight}px`;
78
        } else {
79
            // Scrolling up but notification was manually closed
80
            navbar.style.top = '0';
81
        }
82
        lastScrollTop = scrollTop;
83
    });
84
}
85
86
/**
87
 * Initialize the DNA Helix animation on the homepage
88
 */
89
function initDNAHelix() {
90
    const dnaHelix = document.getElementById('dnaHelix');
91
    if (!dnaHelix) return;
92
    
93
    // Clear any existing content
94
    dnaHelix.innerHTML = '';
95
    
96
    const numBasesUp = 15; // Number of bases to extend upward
97
    const numBasesDown = 25; // Number of bases to extend downward
98
    const totalBases = numBasesUp + numBasesDown;
99
    const basePairs = [
100
        ['A', 'T'],
101
        ['T', 'A'],
102
        ['G', 'C'],
103
        ['C', 'G']
104
    ];
105
    
106
    // Create all base pair elements first but don't append to DOM yet
107
    const fragments = document.createDocumentFragment();
108
    const allBasePairs = [];
109
    
110
    // Create base pairs with a proper twisted helix structure
111
    // Start with negative indices for "upward" extension, then go to positive for "downward"
112
    for (let i = -numBasesUp; i < numBasesDown; i++) {
113
        const pairIndex = Math.floor(Math.random() * basePairs.length);
114
        const [leftBase, rightBase] = basePairs[pairIndex];
115
        
116
        const basePair = document.createElement('div');
117
        basePair.className = 'base-pair';
118
        
119
        // Position each base pair - adjust vertical position to account for bases extending upward
120
        basePair.style.top = `${(i + numBasesUp) * 25}px`;
121
        
122
        // Calculate initial rotation angle to create the helix twist
123
        // Each pair is rotated an incremental amount for the helix effect
124
        const angle = (i * 25) % 360;
125
        
126
        // Calculate X offset based on the angle to create the curve effect
127
        const xOffset = Math.sin(angle * Math.PI / 180) * 20;
128
        
129
        // Start at the target position directly instead of animating to it
130
        basePair.style.transform = `rotateY(${angle}deg) translateZ(40px) translateX(${xOffset}px)`;
131
        
132
        // Set initial opacity to 0 to avoid flash of content
133
        basePair.style.opacity = '0';
134
        
135
        // Create elements
136
        const leftBaseElem = document.createElement('div');
137
        leftBaseElem.className = 'base left-base';
138
        leftBaseElem.textContent = leftBase;
139
        
140
        const rightBaseElem = document.createElement('div');
141
        rightBaseElem.className = 'base right-base';
142
        rightBaseElem.textContent = rightBase;
143
        
144
        const connector = document.createElement('div');
145
        connector.className = 'base-connector';
146
        
147
        basePair.appendChild(leftBaseElem);
148
        basePair.appendChild(connector);
149
        basePair.appendChild(rightBaseElem);
150
        
151
        fragments.appendChild(basePair);
152
        allBasePairs.push(basePair);
153
    }
154
    
155
    // Add all elements to the DOM at once to minimize reflow
156
    dnaHelix.appendChild(fragments);
157
    
158
    // Adjust the container height to accommodate the extended helix
159
    dnaHelix.style.height = `${totalBases * 25 + 50}px`;
160
    
161
    // Add animations with staggered delays after elements are in the DOM
162
    // This ensures smooth animation start
163
    requestAnimationFrame(() => {
164
        allBasePairs.forEach((basePair, index) => {
165
            // Stagger animation delays for a smooth wave effect
166
            const delay = (index * -0.1).toFixed(2);
167
            
168
            // Apply animation now that the element is in the DOM
169
            basePair.style.animation = `rotate 8s linear infinite ${delay}s`;
170
            
171
            // Fade in elements smoothly over time
172
            setTimeout(() => {
173
                basePair.style.opacity = '1';
174
                basePair.style.transition = 'opacity 0.5s ease-in-out';
175
            }, 50 * index);
176
        });
177
    });
178
}
179
180
/**
181
 * Initialize the mobile menu toggle functionality
182
 */
183
function initMobileMenu() {
184
    const mobileToggle = document.getElementById('mobileToggle');
185
    const navLinks = document.getElementById('navLinks');
186
    
187
    if (!mobileToggle || !navLinks) return;
188
    
189
    mobileToggle.addEventListener('click', function() {
190
        navLinks.classList.toggle('active');
191
        
192
        // Change the icon based on the state
193
        const icon = mobileToggle.querySelector('i');
194
        if (navLinks.classList.contains('active')) {
195
            icon.classList.remove('fa-bars');
196
            icon.classList.add('fa-times');
197
        } else {
198
            icon.classList.remove('fa-times');
199
            icon.classList.add('fa-bars');
200
        }
201
    });
202
    
203
    // Close the mobile menu when clicking a link
204
    const links = navLinks.querySelectorAll('a');
205
    links.forEach(link => {
206
        link.addEventListener('click', function() {
207
            navLinks.classList.remove('active');
208
            const icon = mobileToggle.querySelector('i');
209
            icon.classList.remove('fa-times');
210
            icon.classList.add('fa-bars');
211
        });
212
    });
213
}
214
215
/**
216
 * Initialize the navbar scroll effect
217
 */
218
function initNavbarScroll() {
219
    const navbar = document.getElementById('navbar');
220
    if (!navbar) return;
221
    
222
    window.addEventListener('scroll', function() {
223
        if (window.scrollY > 50) {
224
            navbar.classList.add('scrolled');
225
        } else {
226
            navbar.classList.remove('scrolled');
227
        }
228
    });
229
}
230
231
/**
232
 * Animate number counting from 0 to target
233
 * @param {HTMLElement} element - The element containing the number
234
 * @param {number} target - The target number to count to
235
 * @param {string} suffix - Optional suffix like '%' or 'M'
236
 * @param {number} duration - Animation duration in milliseconds
237
 */
238
function animateNumber(element, target, suffix = '', duration = 2000) {
239
    if (!element) return;
240
    
241
    let start = 0;
242
    let startTime = null;
243
    const targetNum = parseFloat(target);
244
    
245
    function easeOutQuart(x) {
246
        return 1 - Math.pow(1 - x, 4);
247
    }
248
    
249
    function animate(timestamp) {
250
        if (!startTime) startTime = timestamp;
251
        
252
        const progress = Math.min((timestamp - startTime) / duration, 1);
253
        const easedProgress = easeOutQuart(progress);
254
        
255
        let currentValue = Math.floor(easedProgress * targetNum);
256
        
257
        // Handle special cases
258
        if (suffix === '%') {
259
            const decimal = (easedProgress * targetNum) % 1;
260
            if (decimal > 0) {
261
                currentValue = (easedProgress * targetNum).toFixed(1);
262
            }
263
        }
264
        
265
        element.textContent = `${currentValue}${suffix}`;
266
        
267
        if (progress < 1) {
268
            requestAnimationFrame(animate);
269
        } else {
270
            element.textContent = `${target}${suffix}`;
271
        }
272
    }
273
    
274
    requestAnimationFrame(animate);
275
}
276
277
/**
278
 * Initialize the stats counter animation with Intersection Observer
279
 */
280
function initStatsAnimation() {
281
    const statsSection = document.querySelector('.stats-section');
282
    if (!statsSection) return;
283
    
284
    const statElements = {
285
        accuracy: { elem: document.getElementById('statAccuracy'), target: '141', suffix: '' },
286
        sequences: { elem: document.getElementById('statSequences'), target: '7', suffix: 'M+' },
287
        users: { elem: document.getElementById('statUsers'), target: '46', suffix: '+' }
288
    };
289
    
290
    let animated = false;
291
    
292
    const observer = new IntersectionObserver((entries) => {
293
        entries.forEach(entry => {
294
            if (entry.isIntersecting && !animated) {
295
                // Animate each stat with staggered delays
296
                Object.values(statElements).forEach((stat, index) => {
297
                    setTimeout(() => {
298
                        if (stat.elem) {
299
                            animateNumber(stat.elem, stat.target, stat.suffix, 2000);
300
                        }
301
                    }, index * 200);
302
                });
303
                
304
                animated = true;
305
                observer.unobserve(statsSection);
306
            }
307
        });
308
    }, { threshold: 0.5 });
309
    
310
    observer.observe(statsSection);
311
}
312
313
/**
314
 * Add smooth scrolling for anchor links
315
 */
316
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
317
    anchor.addEventListener('click', function(e) {
318
        const targetId = this.getAttribute('href');
319
        if (targetId === '#') return; // Skip if it's just "#"
320
        
321
        const targetElement = document.querySelector(targetId);
322
        if (targetElement) {
323
            e.preventDefault();
324
            
325
            window.scrollTo({
326
                top: targetElement.offsetTop - 100,
327
                behavior: 'smooth'
328
            });
329
        }
330
    });
331
});
332
333
/**
334
 * Initialize navbar pulse effect for futuristic look
335
 */
336
function initNavbarPulse() {
337
    const navbar = document.getElementById('navbar');
338
    if (!navbar) return;
339
    setInterval(() => {
340
        navbar.classList.toggle('navbar-pulse');
341
    }, 2000);
342
}
343
344
/**
345
 * Initialize futuristic notification effects on the notification banner
346
 */
347
function initFuturisticNotificationEffects() {
348
    const banner = document.querySelector('.notification-banner');
349
    if (!banner) return;
350
    banner.addEventListener('mouseenter', () => {
351
        banner.style.transition = 'filter 0.3s ease';
352
        banner.style.filter = 'brightness(1.2) contrast(1.1)';
353
    });
354
    banner.addEventListener('mouseleave', () => {
355
        banner.style.filter = 'none';
356
    });
357
}
358
359
/**
360
 * Add intersection observer for animating sections as they come into view
361
 */
362
const animateSections = document.querySelectorAll('.section, .feature-card, .card');
363
const observerOptions = {
364
    threshold: 0.1,
365
    rootMargin: '0px 0px -100px 0px'
366
};
367
368
const sectionObserver = new IntersectionObserver((entries) => {
369
    entries.forEach(entry => {
370
        if (entry.isIntersecting) {
371
            entry.target.style.opacity = '1';
372
            entry.target.style.transform = 'translateY(0)';
373
            sectionObserver.unobserve(entry.target);
374
        }
375
    });
376
}, observerOptions);
377
378
animateSections.forEach(section => {
379
    section.style.opacity = '0';
380
    section.style.transform = 'translateY(20px)';
381
    section.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
382
    sectionObserver.observe(section);
383
});