230 lines
7.8 KiB
JavaScript
230 lines
7.8 KiB
JavaScript
/* ================================================
|
||
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 });
|