Add estimates package modals, magical animations and pricing links

- estimates: per-package detail modals with min/max breakdown and specs,
  opening with a wand-flick spiral spring and sparkle trail, closing with a
  perimeter sparkle burst
- home: pricing teaser section linking to estimates; contact form pre-fills
  a message when arriving from a package "Enquire" link
- mascot: Star Trek-style teleport-in (delay, sparkle charge, bottom-up
  materialise with near-white energy wash and rising sparkle beam)
- buttons: wand-sparkle swipe speed and density now scale with button width
- themed scrollbars site-wide with stable gutter to stop modal scroll shift

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Fraxle
2026-06-02 12:38:41 +01:00
co-authored by Claude Opus 4.8
parent 206daaef5d
commit 9303dc7f4d
4 changed files with 724 additions and 6 deletions
+158 -3
View File
@@ -8,7 +8,7 @@ if (history.scrollRestoration) {
}
(function () {
var params = new URLSearchParams(window.location.search);
if (!params.has('sent') && !params.has('error')) {
if (!params.has('sent') && !params.has('error') && !params.has('package')) {
window.scrollTo(0, 0);
if (window.location.hash) {
history.replaceState(null, '', window.location.pathname);
@@ -16,6 +16,41 @@ if (history.scrollRestoration) {
}
})();
// --- 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');
@@ -167,7 +202,12 @@ function triggerWandEffect(btn) {
var btnW = btn.offsetWidth;
var btnH = btn.offsetHeight;
var duration = 520;
// 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;
@@ -179,7 +219,7 @@ function triggerWandEffect(btn) {
var p = Math.min(elapsed / duration, 1);
var x = p * btnW;
var ci = Math.floor(elapsed / 36);
var ci = Math.floor(p * CLUSTERS);
if (ci > lastCluster) {
lastCluster = ci;
spawnSparkleCluster(btn, x, btnH);
@@ -227,3 +267,118 @@ window.addEventListener('scroll', function () {
: '';
});
}, { 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.3 },
{ transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.7 },
{ transform: 'translate(-50%,-50%) scale(0)' }
], { duration: life, easing: 'ease-in-out' }).onfinish = function () { s.remove(); };
}
function teleport(img) {
var r = img.getBoundingClientRect();
var START_DELAY = 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
// 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';
}
}
requestAnimationFrame(frame);
}, START_DELAY);
}
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); });
})();