/* ================================================ FRAXLE.NET — main.js ================================================ */ // --- Scroll position: always start at top unless coming from form --- if (history.scrollRestoration) { history.scrollRestoration = 'manual'; } (function () { var params = new URLSearchParams(window.location.search); if (!params.has('sent') && !params.has('error')) { window.scrollTo(0, 0); if (window.location.hash) { history.replaceState(null, '', window.location.pathname); } } })(); // --- Hero starfield --- (function () { const container = document.getElementById('hero-stars'); if (!container) return; const style = document.createElement('style'); style.textContent = ` @keyframes twinkle { 0%, 100% { opacity: 0.2; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.3); } } @keyframes shootingStar { 0% { transform: translateX(0) translateY(0); opacity: 1; } 100% { transform: translateX(200px) translateY(80px); opacity: 0; } } `; document.head.appendChild(style); // Static stars for (let i = 0; i < 160; i++) { const s = document.createElement('div'); const size = Math.random() * 2.5 + 0.5; s.style.cssText = [ 'position:absolute', `width:${size}px`, `height:${size}px`, `background:rgba(255,255,255,${(Math.random() * 0.5 + 0.3).toFixed(2)})`, 'border-radius:50%', `top:${(Math.random() * 100).toFixed(2)}%`, `left:${(Math.random() * 100).toFixed(2)}%`, `animation:twinkle ${(Math.random() * 3 + 2).toFixed(1)}s ease-in-out ${(Math.random() * 5).toFixed(1)}s infinite`, ].join(';'); container.appendChild(s); } // Gold sparkles for (let i = 0; i < 28; i++) { const s = document.createElement('div'); s.style.cssText = [ 'position:absolute', 'width:2px', 'height:3px', 'background:rgba(251,191,36,0.9)', 'border-radius:50%', `top:${(Math.random() * 100).toFixed(2)}%`, `left:${(Math.random() * 100).toFixed(2)}%`, `animation:twinkle ${(Math.random() * 2 + 1.5).toFixed(1)}s ease-in-out ${(Math.random() * 4).toFixed(1)}s infinite`, ].join(';'); container.appendChild(s); } })(); // --- Nav scroll effect --- const header = document.getElementById('site-header'); window.addEventListener('scroll', function () { header.classList.toggle('scrolled', window.scrollY > 60); }, { passive: true }); // --- Hamburger menu --- const hamburger = document.getElementById('nav-hamburger'); const navLinks = document.getElementById('nav-links'); if (hamburger && navLinks) { hamburger.addEventListener('click', function () { const isOpen = navLinks.classList.toggle('open'); hamburger.classList.toggle('open', isOpen); hamburger.setAttribute('aria-expanded', isOpen); document.body.style.overflow = isOpen ? 'hidden' : ''; }); // Close on link click navLinks.querySelectorAll('a').forEach(function (link) { link.addEventListener('click', function () { navLinks.classList.remove('open'); hamburger.classList.remove('open'); hamburger.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; }); }); // Close on outside click document.addEventListener('click', function (e) { if (!hamburger.contains(e.target) && !navLinks.contains(e.target)) { navLinks.classList.remove('open'); hamburger.classList.remove('open'); hamburger.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; } }); } // --- Scroll reveal --- const reveals = document.querySelectorAll('.reveal'); if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add('visible'); observer.unobserve(entry.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); reveals.forEach(function (el) { observer.observe(el); }); } else { // Fallback: show all reveals.forEach(function (el) { el.classList.add('visible'); }); } // --- Magic wand button effect --- var WAND_CHARS = ['✦', '✧', '✶', '✦', '✧']; var WAND_COLORS = [ 'rgba(255,255,255,1)', 'rgba(251,191,36,1)', 'rgba(253,224,120,1)', 'rgba(255,255,255,0.88)', ]; function spawnSparkleCluster(btn, x, btnH) { var n = 14 + Math.floor(Math.random() * 13); // 16–28 per cluster for (var i = 0; i < n; i++) { var s = document.createElement('span'); s.className = 'btn-sparkle'; var ox = (Math.random() - 0.5) * 40; // ±20px horizontal spread var fy = Math.random() * btnH - 7; // shifted up a few px var sz = (0.28 + Math.random() * 0.55).toFixed(2); var dur = (0.38 + Math.random() * 0.45).toFixed(3); var col = WAND_COLORS[Math.floor(Math.random() * WAND_COLORS.length)]; var ch = WAND_CHARS [Math.floor(Math.random() * WAND_CHARS.length)]; s.textContent = ch; s.style.cssText = [ 'left:' + (x + ox).toFixed(1) + 'px', 'top:' + fy.toFixed(1) + 'px', 'font-size:' + sz + 'rem', 'color:' + col, 'text-shadow:0 0 5px ' + col + ',0 0 11px rgba(255,255,255,0.35)', '--dur:' + dur + 's', ].join(';'); btn.appendChild(s); (function (el, d) { setTimeout(function () { if (el.parentNode) el.remove(); }, (parseFloat(d) + 0.1) * 1000); }(s, dur)); } } function triggerWandEffect(btn) { if (btn.dataset.wandActive) return; btn.dataset.wandActive = '1'; var btnW = btn.offsetWidth; var btnH = btn.offsetHeight; var duration = 520; var start = null; var lastCluster = -1; function ease(t) { return t < 0.5 ? 2*t*t : -1 + (4 - 2*t) * t; } function frame(ts) { if (!start) start = ts; var elapsed = ts - start; var p = Math.min(elapsed / duration, 1); var x = p * btnW; var ci = Math.floor(elapsed / 36); if (ci > lastCluster) { lastCluster = ci; spawnSparkleCluster(btn, x, btnH); } if (p < 1) { requestAnimationFrame(frame); } else { setTimeout(function () { delete btn.dataset.wandActive; }, 60); } } requestAnimationFrame(frame); } document.querySelectorAll('.btn').forEach(function (btn) { btn.addEventListener('mouseenter', function () { triggerWandEffect(btn); }); }); // --- Stagger cards/rows within grids and lists --- document.querySelectorAll('.services-grid, .projects-list').forEach(function (grid) { grid.querySelectorAll('.reveal').forEach(function (card, i) { card.style.transitionDelay = (i * 0.1) + 's'; }); }); // --- Smooth active nav highlight on scroll --- const sections = document.querySelectorAll('section[id]'); const navAnchors = document.querySelectorAll('.nav-links a[href^="#"]'); window.addEventListener('scroll', function () { let current = ''; sections.forEach(function (sec) { if (window.scrollY >= sec.offsetTop - 120) { current = sec.getAttribute('id'); } }); navAnchors.forEach(function (a) { a.style.color = a.getAttribute('href') === '#' + current ? 'var(--white)' : ''; }); }, { passive: true });