|
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 |
}); |