Files
fraxlenet/js/main.js
T
2026-06-02 12:55:49 +01:00

444 lines
19 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') && !params.has('package')) {
window.scrollTo(0, 0);
if (window.location.hash) {
history.replaceState(null, '', window.location.pathname);
}
}
})();
// --- Pre-fill contact form when arriving from a pricing package ---
(function () {
var params = new URLSearchParams(window.location.search);
var pkg = params.get('package');
if (!pkg) return;
var MESSAGES = {
'first-spark': "Hi Danny! I'm interested in The First Spark website package (from £250). A little about my project:\n\n",
'enchanted-workshop': "Hi Danny! I'm interested in The Enchanted Workshop website package (£750–£1,500). A little about my project:\n\n",
'arcane-forge': "Hi Danny! I'm interested in The Arcane Forge website package (£1,500–£3,000). A little about my project:\n\n",
'merchants-portal': "Hi Danny! I'm interested in The Merchant's Portal ecommerce package (£3,000–£5,000). A little about my project:\n\n",
'grand-emporium': "Hi Danny! I'm interested in The Grand Emporium ecommerce package (£5,000–£7,500+). A little about my project:\n\n",
'wizards-bench': "Hi Danny! I'd like some ad-hoc tech support from The Wizard's Bench (£40/hour). Here's what I need help with:\n\n",
'minor-ward': "Hi Danny! I'm interested in the Minor Ward care plan (£10/month). A little about my website:\n\n",
'greater-ward': "Hi Danny! I'm interested in the Greater Ward care plan (£25/month). A little about my website:\n\n",
'merchants-ward': "Hi Danny! I'm interested in the Merchant's Ward care plan (£50/month). A little about my store:\n\n"
};
var field = document.getElementById('message');
if (field && MESSAGES[pkg]) {
field.value = MESSAGES[pkg];
}
var contact = document.getElementById('contact');
if (contact) {
requestAnimationFrame(function () { contact.scrollIntoView(); });
}
if (field && MESSAGES[pkg]) {
field.focus();
var len = field.value.length;
try { field.setSelectionRange(len, len); } catch (e) {}
}
})();
// --- 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); // 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 - 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;
// Scale swipe speed and sparkle density to the button width so narrow and wide buttons
// feel consistent: constant px/ms sweep speed, and constant spacing between sparkle bursts.
var SPEED = 0.73; // px per ms the swipe travels
var SPACING = 15; // px between sparkle bursts
var duration = Math.max(160, Math.min(btnW / SPEED, 720)); // clamp for tiny / huge buttons
var CLUSTERS = Math.max(3, Math.round(btnW / SPACING)); // fewer bursts on narrow buttons
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(p * CLUSTERS);
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 });
// --- Mascot "transporter" teleport-in ---
(function () {
var imgs = document.querySelectorAll('.mascot-teleport');
if (!imgs.length) return;
var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (REDUCED) {
imgs.forEach(function (im) { im.classList.remove('mascot-teleport'); im.style.opacity = '1'; });
return;
}
var T_CHARS = ['✦', '✧', '✶', '✩', '✨', '⋆'];
var T_COLORS = ['rgba(255,255,255,1)', 'rgba(251,191,36,1)', 'rgba(253,224,120,1)', 'rgba(45,212,191,1)']; // white, gold, teal
function teleSpark(x, y, life) {
var s = document.createElement('span');
var size = (0.45 + Math.random() * 0.8).toFixed(2);
var col = T_COLORS[(Math.random() * T_COLORS.length) | 0];
s.textContent = T_CHARS[(Math.random() * T_CHARS.length) | 0];
s.style.cssText = 'position:fixed;left:' + x + 'px;top:' + y + 'px;z-index:1200;pointer-events:none;line-height:1;'
+ 'font-size:' + size + 'rem;color:' + col + ';text-shadow:0 0 4px ' + col + ',0 0 2px rgba(255,255,255,0.95);'
+ 'will-change:transform,opacity;';
document.body.appendChild(s);
s.animate([
{ transform: 'translate(-50%,-50%) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.08 }, // pop in almost instantly
{ transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.6 },
{ transform: 'translate(-50%,-50%) scale(0)' }
], { duration: life, easing: 'ease-out' }).onfinish = function () { s.remove(); };
}
function teleport(img, startDelay) {
var r = img.getBoundingClientRect();
var START_DELAY = (typeof startDelay === 'number') ? startDelay : 500; // pause before anything happens
var CHARGE = 1000; // sparkles gather along the bottom before the figure grows
var GROW = 2200; // the bottom-up materialise
var baseShadow = 'drop-shadow(0 8px 24px rgba(0,0,0,0.35))';
function ease(t) { return t * t; } // ease-in: gentle at the bottom, no slow-down toward the top
img.dataset.teleBusy = '1';
// Keep it invisible (clipped) but opaque from the outset, so nothing flashes during the delay
img.style.opacity = '1';
img.style.webkitClipPath = img.style.clipPath = 'inset(100% 0 0 0)';
setTimeout(function () {
var start = null;
function frame(ts) {
if (!start) start = ts;
var el = ts - start;
// --- Charge phase: just sparkles building along the bottom edge ---
if (el < CHARGE) {
var cp = el / CHARGE; // 0..1 through the charge
var cn = Math.round(1 + 5 * cp); // build up density toward the grow
for (var c = 0; c < cn; c++) {
var cx = r.left + Math.random() * r.width;
var cy = r.bottom - Math.random() * r.height * 0.06; // hug the base
teleSpark(cx, cy, 480 + Math.random() * 360);
}
requestAnimationFrame(frame);
return;
}
// --- Grow phase: bottom-up reveal + rising beam ---
var raw = Math.min((el - CHARGE) / GROW, 1);
var p = ease(raw);
var clip = ((1 - p) * 100).toFixed(2);
img.style.webkitClipPath = img.style.clipPath = 'inset(' + clip + '% 0 0 0)';
// Bright near-white energy wash that holds, then settles to the normal drop-shadow
var hx = 1 - p;
var hold = hx * hx * hx * (hx * (hx * 6 - 15) + 10); // smootherstep(1-p): holds, then fades smoothly
var bright = (1 + 1.8 * hold).toFixed(2); // up to ~2.8 — near white-hot
var wash = (1 - 0.55 * hold).toFixed(2); // desaturate early for a whiter look
var glow = (0.85 * hold).toFixed(2);
img.style.filter = baseShadow
+ ' drop-shadow(0 0 16px rgba(255,255,255,' + glow + '))'
+ ' brightness(' + bright + ') saturate(' + wash + ')';
// Sparkle beam sitting on the rising materialisation edge
var lineY = r.bottom - p * r.height;
if (raw < 0.98) {
var n = Math.round(1 + 8 * (1 - p)); // dense at the start (beam low), sparse as it rises
for (var i = 0; i < n; i++) {
var x = r.left + Math.random() * r.width;
var y = (Math.random() < 0.7)
? lineY + (Math.random() - 0.5) * r.height * 0.12 // tight on the beam line
: lineY - Math.random() * Math.max(0, lineY - r.top); // shimmer in the un-formed area above
y = Math.max(r.top, Math.min(r.bottom, y));
var vf = (y - r.top) / r.height; // 0 at top, 1 at bottom
teleSpark(x, y, (480 + Math.random() * 360) * (0.4 + 0.6 * vf)); // fade quicker higher up
}
}
if (raw < 1) {
requestAnimationFrame(frame);
} else {
img.style.webkitClipPath = img.style.clipPath = '';
img.style.filter = ''; // CSS drop-shadow returns
img.classList.remove('mascot-teleport');
img.style.opacity = '1';
delete img.dataset.teleBusy;
}
}
requestAnimationFrame(frame);
}, START_DELAY);
}
// Reverse "beam out": dissolve top-down into a puff of sparkles, then stay hidden
function dematerialize(img, done) {
var r = img.getBoundingClientRect();
var DUR = 800;
var baseShadow = 'drop-shadow(0 8px 24px rgba(0,0,0,0.35))';
var start = null;
function frame(ts) {
if (!start) start = ts;
var p = Math.min((ts - start) / DUR, 1);
// Dissolve away from the top downwards
img.style.webkitClipPath = img.style.clipPath = 'inset(' + (p * 100).toFixed(2) + '% 0 0 0)';
// Energy flash that builds as it beams out
var bright = (1 + 1.2 * p).toFixed(2);
var glow = (0.7 * p).toFixed(2);
img.style.filter = baseShadow + ' drop-shadow(0 0 14px rgba(255,255,255,' + glow + ')) brightness(' + bright + ')';
// Sparkles riding the dissolving edge
var lineY = r.top + p * r.height;
if (p < 0.98) {
for (var i = 0; i < 6; i++) {
var x = r.left + Math.random() * r.width;
var y = lineY + (Math.random() - 0.5) * r.height * 0.1;
y = Math.max(r.top, Math.min(r.bottom, y));
teleSpark(x, y, 480 + Math.random() * 360);
}
}
if (p < 1) {
requestAnimationFrame(frame);
} else {
// A final puff of sparkles where it stood
for (var j = 0; j < 16; j++) {
teleSpark(r.left + Math.random() * r.width, r.top + Math.random() * r.height, 420 + Math.random() * 360);
}
img.style.opacity = '0'; // stay hidden during the wait
img.style.webkitClipPath = img.style.clipPath = '';
img.style.filter = '';
if (done) done();
}
}
requestAnimationFrame(frame);
}
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { io.unobserve(e.target); teleport(e.target); }
});
}, { threshold: 0.25 });
imgs.forEach(function (im) {
io.observe(im);
// Click to beam out, then re-materialise after ~3s
im.style.cursor = 'pointer';
im.addEventListener('click', function () {
if (im.dataset.teleBusy) return; // ignore while an effect is already running
im.dataset.teleBusy = '1';
dematerialize(im, function () {
setTimeout(function () { teleport(im, 0); }, 3000);
});
});
});
})();