Files
fraxlenet/js/main.js
T
2026-05-30 00:02:13 +01:00

229 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ================================================
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:3px', '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 = 16 + Math.floor(Math.random() * 13); // 1628 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 - 8; // 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 });