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

Switch to side-by-side view

--- a
+++ b/web/index.js
@@ -0,0 +1,383 @@
+/**
+ * DNAnalyzer Main JavaScript
+ * Handles animations, interactivity and UI functionality
+ */
+
+document.addEventListener('DOMContentLoaded', function() {
+    // Initialize DNA Helix animation
+    initDNAHelix();
+    
+    // Initialize mobile menu toggle
+    initMobileMenu();
+    
+    // Initialize navbar scroll effect
+    initNavbarScroll();
+    
+    // Initialize stats counter animation
+    initStatsAnimation();
+    
+    // Initialize notification banner dismiss functionality
+    initNotificationBanner();
+
+    // Initialize notification banner scroll behavior
+    initNotificationScroll();
+    
+    // Initialize navbar pulse effect
+    initNavbarPulse();
+});
+
+// Shared state for notification banner
+let notificationClosed = false;
+
+/**
+ * Initialize the notification banner dismiss functionality and adjust navbar positioning
+ */
+function initNotificationBanner() {
+    const banner = document.querySelector('.notification-banner');
+    const navbar = document.getElementById('navbar');
+    if (!banner || !navbar) return;
+
+    const bannerHeight = banner.offsetHeight;
+    document.documentElement.style.setProperty('--notification-height', `${bannerHeight}px`);
+
+    const closeBtn = document.querySelector('.notification-banner .notification-close');
+    if (!closeBtn) return;
+
+    closeBtn.addEventListener('click', function() {
+        banner.classList.add('closed');
+        document.documentElement.style.setProperty('--notification-height', '0px');
+        navbar.style.top = '0';
+        notificationClosed = true;
+    });
+
+    // Initial positioning
+    navbar.style.top = `${bannerHeight}px`;
+}
+
+/**
+ * Initialize notification banner scroll behavior
+ */
+function initNotificationScroll() {
+    const banner = document.querySelector('.notification-banner');
+    const navbar = document.getElementById('navbar');
+    if (!banner || !navbar) return;
+
+    let lastScrollTop = 0;
+    const bannerHeight = banner.offsetHeight;
+
+    window.addEventListener('scroll', function() {
+        let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+        if (scrollTop > lastScrollTop && scrollTop > bannerHeight) {
+            // Scrolling down
+            banner.classList.add('hide-notification');
+            navbar.style.top = '0';
+        } else if (!notificationClosed) {
+            // Scrolling up and notification was not manually closed
+            banner.classList.remove('hide-notification');
+            navbar.style.top = `${bannerHeight}px`;
+        } else {
+            // Scrolling up but notification was manually closed
+            navbar.style.top = '0';
+        }
+        lastScrollTop = scrollTop;
+    });
+}
+
+/**
+ * Initialize the DNA Helix animation on the homepage
+ */
+function initDNAHelix() {
+    const dnaHelix = document.getElementById('dnaHelix');
+    if (!dnaHelix) return;
+    
+    // Clear any existing content
+    dnaHelix.innerHTML = '';
+    
+    const numBasesUp = 15; // Number of bases to extend upward
+    const numBasesDown = 25; // Number of bases to extend downward
+    const totalBases = numBasesUp + numBasesDown;
+    const basePairs = [
+        ['A', 'T'],
+        ['T', 'A'],
+        ['G', 'C'],
+        ['C', 'G']
+    ];
+    
+    // Create all base pair elements first but don't append to DOM yet
+    const fragments = document.createDocumentFragment();
+    const allBasePairs = [];
+    
+    // Create base pairs with a proper twisted helix structure
+    // Start with negative indices for "upward" extension, then go to positive for "downward"
+    for (let i = -numBasesUp; i < numBasesDown; i++) {
+        const pairIndex = Math.floor(Math.random() * basePairs.length);
+        const [leftBase, rightBase] = basePairs[pairIndex];
+        
+        const basePair = document.createElement('div');
+        basePair.className = 'base-pair';
+        
+        // Position each base pair - adjust vertical position to account for bases extending upward
+        basePair.style.top = `${(i + numBasesUp) * 25}px`;
+        
+        // Calculate initial rotation angle to create the helix twist
+        // Each pair is rotated an incremental amount for the helix effect
+        const angle = (i * 25) % 360;
+        
+        // Calculate X offset based on the angle to create the curve effect
+        const xOffset = Math.sin(angle * Math.PI / 180) * 20;
+        
+        // Start at the target position directly instead of animating to it
+        basePair.style.transform = `rotateY(${angle}deg) translateZ(40px) translateX(${xOffset}px)`;
+        
+        // Set initial opacity to 0 to avoid flash of content
+        basePair.style.opacity = '0';
+        
+        // Create elements
+        const leftBaseElem = document.createElement('div');
+        leftBaseElem.className = 'base left-base';
+        leftBaseElem.textContent = leftBase;
+        
+        const rightBaseElem = document.createElement('div');
+        rightBaseElem.className = 'base right-base';
+        rightBaseElem.textContent = rightBase;
+        
+        const connector = document.createElement('div');
+        connector.className = 'base-connector';
+        
+        basePair.appendChild(leftBaseElem);
+        basePair.appendChild(connector);
+        basePair.appendChild(rightBaseElem);
+        
+        fragments.appendChild(basePair);
+        allBasePairs.push(basePair);
+    }
+    
+    // Add all elements to the DOM at once to minimize reflow
+    dnaHelix.appendChild(fragments);
+    
+    // Adjust the container height to accommodate the extended helix
+    dnaHelix.style.height = `${totalBases * 25 + 50}px`;
+    
+    // Add animations with staggered delays after elements are in the DOM
+    // This ensures smooth animation start
+    requestAnimationFrame(() => {
+        allBasePairs.forEach((basePair, index) => {
+            // Stagger animation delays for a smooth wave effect
+            const delay = (index * -0.1).toFixed(2);
+            
+            // Apply animation now that the element is in the DOM
+            basePair.style.animation = `rotate 8s linear infinite ${delay}s`;
+            
+            // Fade in elements smoothly over time
+            setTimeout(() => {
+                basePair.style.opacity = '1';
+                basePair.style.transition = 'opacity 0.5s ease-in-out';
+            }, 50 * index);
+        });
+    });
+}
+
+/**
+ * Initialize the mobile menu toggle functionality
+ */
+function initMobileMenu() {
+    const mobileToggle = document.getElementById('mobileToggle');
+    const navLinks = document.getElementById('navLinks');
+    
+    if (!mobileToggle || !navLinks) return;
+    
+    mobileToggle.addEventListener('click', function() {
+        navLinks.classList.toggle('active');
+        
+        // Change the icon based on the state
+        const icon = mobileToggle.querySelector('i');
+        if (navLinks.classList.contains('active')) {
+            icon.classList.remove('fa-bars');
+            icon.classList.add('fa-times');
+        } else {
+            icon.classList.remove('fa-times');
+            icon.classList.add('fa-bars');
+        }
+    });
+    
+    // Close the mobile menu when clicking a link
+    const links = navLinks.querySelectorAll('a');
+    links.forEach(link => {
+        link.addEventListener('click', function() {
+            navLinks.classList.remove('active');
+            const icon = mobileToggle.querySelector('i');
+            icon.classList.remove('fa-times');
+            icon.classList.add('fa-bars');
+        });
+    });
+}
+
+/**
+ * Initialize the navbar scroll effect
+ */
+function initNavbarScroll() {
+    const navbar = document.getElementById('navbar');
+    if (!navbar) return;
+    
+    window.addEventListener('scroll', function() {
+        if (window.scrollY > 50) {
+            navbar.classList.add('scrolled');
+        } else {
+            navbar.classList.remove('scrolled');
+        }
+    });
+}
+
+/**
+ * Animate number counting from 0 to target
+ * @param {HTMLElement} element - The element containing the number
+ * @param {number} target - The target number to count to
+ * @param {string} suffix - Optional suffix like '%' or 'M'
+ * @param {number} duration - Animation duration in milliseconds
+ */
+function animateNumber(element, target, suffix = '', duration = 2000) {
+    if (!element) return;
+    
+    let start = 0;
+    let startTime = null;
+    const targetNum = parseFloat(target);
+    
+    function easeOutQuart(x) {
+        return 1 - Math.pow(1 - x, 4);
+    }
+    
+    function animate(timestamp) {
+        if (!startTime) startTime = timestamp;
+        
+        const progress = Math.min((timestamp - startTime) / duration, 1);
+        const easedProgress = easeOutQuart(progress);
+        
+        let currentValue = Math.floor(easedProgress * targetNum);
+        
+        // Handle special cases
+        if (suffix === '%') {
+            const decimal = (easedProgress * targetNum) % 1;
+            if (decimal > 0) {
+                currentValue = (easedProgress * targetNum).toFixed(1);
+            }
+        }
+        
+        element.textContent = `${currentValue}${suffix}`;
+        
+        if (progress < 1) {
+            requestAnimationFrame(animate);
+        } else {
+            element.textContent = `${target}${suffix}`;
+        }
+    }
+    
+    requestAnimationFrame(animate);
+}
+
+/**
+ * Initialize the stats counter animation with Intersection Observer
+ */
+function initStatsAnimation() {
+    const statsSection = document.querySelector('.stats-section');
+    if (!statsSection) return;
+    
+    const statElements = {
+        accuracy: { elem: document.getElementById('statAccuracy'), target: '141', suffix: '' },
+        sequences: { elem: document.getElementById('statSequences'), target: '7', suffix: 'M+' },
+        users: { elem: document.getElementById('statUsers'), target: '46', suffix: '+' }
+    };
+    
+    let animated = false;
+    
+    const observer = new IntersectionObserver((entries) => {
+        entries.forEach(entry => {
+            if (entry.isIntersecting && !animated) {
+                // Animate each stat with staggered delays
+                Object.values(statElements).forEach((stat, index) => {
+                    setTimeout(() => {
+                        if (stat.elem) {
+                            animateNumber(stat.elem, stat.target, stat.suffix, 2000);
+                        }
+                    }, index * 200);
+                });
+                
+                animated = true;
+                observer.unobserve(statsSection);
+            }
+        });
+    }, { threshold: 0.5 });
+    
+    observer.observe(statsSection);
+}
+
+/**
+ * Add smooth scrolling for anchor links
+ */
+document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+    anchor.addEventListener('click', function(e) {
+        const targetId = this.getAttribute('href');
+        if (targetId === '#') return; // Skip if it's just "#"
+        
+        const targetElement = document.querySelector(targetId);
+        if (targetElement) {
+            e.preventDefault();
+            
+            window.scrollTo({
+                top: targetElement.offsetTop - 100,
+                behavior: 'smooth'
+            });
+        }
+    });
+});
+
+/**
+ * Initialize navbar pulse effect for futuristic look
+ */
+function initNavbarPulse() {
+    const navbar = document.getElementById('navbar');
+    if (!navbar) return;
+    setInterval(() => {
+        navbar.classList.toggle('navbar-pulse');
+    }, 2000);
+}
+
+/**
+ * Initialize futuristic notification effects on the notification banner
+ */
+function initFuturisticNotificationEffects() {
+    const banner = document.querySelector('.notification-banner');
+    if (!banner) return;
+    banner.addEventListener('mouseenter', () => {
+        banner.style.transition = 'filter 0.3s ease';
+        banner.style.filter = 'brightness(1.2) contrast(1.1)';
+    });
+    banner.addEventListener('mouseleave', () => {
+        banner.style.filter = 'none';
+    });
+}
+
+/**
+ * Add intersection observer for animating sections as they come into view
+ */
+const animateSections = document.querySelectorAll('.section, .feature-card, .card');
+const observerOptions = {
+    threshold: 0.1,
+    rootMargin: '0px 0px -100px 0px'
+};
+
+const sectionObserver = new IntersectionObserver((entries) => {
+    entries.forEach(entry => {
+        if (entry.isIntersecting) {
+            entry.target.style.opacity = '1';
+            entry.target.style.transform = 'translateY(0)';
+            sectionObserver.unobserve(entry.target);
+        }
+    });
+}, observerOptions);
+
+animateSections.forEach(section => {
+    section.style.opacity = '0';
+    section.style.transform = 'translateY(20px)';
+    section.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
+    sectionObserver.observe(section);
+});
\ No newline at end of file