Initial project structure

This commit is contained in:
Fraxle
2026-05-30 00:02:13 +01:00
commit 9ac8763f66
7 changed files with 1531 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
/* ================================================
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 });