up/docs/javascripts/extra.js

162 lines
4.6 KiB
JavaScript
Raw Normal View History

/**
* God Spark Zensical Theme Kit (JS)
* Zero dependencies. ~1.5 KB minified.
*
* Features
* IntersectionObserver scroll-reveal (class: .godspark-reveal)
* Optional cursor glow (enable via [godspark-glow] attribute)
* Reduced-motion fallback baked in
*/
;(function () {
'use strict';
/* ── Reduced-motion preference (shared) ── */
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
/* ── 1. Scroll Reveal via IntersectionObserver ── */
function initScrollReveal () {
if (prefersReducedMotion) return; // already handled in CSS
const reveals = document.querySelectorAll('.godspark-reveal');
if (!reveals.length) return;
// Respect any user-set threshold via CSS variable, default 12%
const threshold = parseFloat(
getComputedStyle(document.documentElement)
.getPropertyValue('--godspark-reveal-threshold')
) || 0.12;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); // fire once
}
});
},
{ threshold, rootMargin: '0px 0px -40px 0px' }
);
reveals.forEach((el) => observer.observe(el));
}
/* ── 2. Cursor Glow ── */
function initCursorGlow () {
// Only run if [godspark-glow] is present on <html> or <body>
if (!document.documentElement.hasAttribute('godspark-glow') &&
!document.body.hasAttribute('godspark-glow')) {
return;
}
if (prefersReducedMotion) return; // CSS already hides it
// Add a class for the CSS ::after pseudo-element to pick up
document.documentElement.classList.add('godspark-glow-active');
const glow = document.querySelector('.godspark-glow-active::after');
// We track position via a CSS custom property on the root element,
// letting the ::after pseudo consume it.
const styleEl = document.createElement('style');
styleEl.id = 'godspark-glow-dynamic';
document.head.appendChild(styleEl);
let mouseX = -400;
let mouseY = -400;
let rafPending = false;
function setGlowPosition (x, y) {
mouseX = x;
mouseY = y;
updateGlowStyle();
}
function updateGlowStyle () {
styleEl.textContent = `
:root {
--godspark-cursor-x: ${mouseX}px;
--godspark-cursor-y: ${mouseY}px;
}
`;
}
// Generate a unique glow element per page to act as the cursor-follower
const cursorEl = document.createElement('div');
cursorEl.setAttribute('aria-hidden', 'true');
cursorEl.className = 'godspark-cursor-glow';
cursorEl.style.cssText = `
position: fixed;
left: ${mouseX}px;
top: ${mouseY}px;
width: 380px;
height: 380px;
border-radius: 50%;
pointer-events: none;
z-index: 999999;
background: radial-gradient(circle,
rgba(195,174,214,0.22) 0%,
rgba(184,169,232,0.08) 35%,
transparent 70%
);
transform: translate(-50%, -50%);
touch-action: none;
will-change: left, top;
transition: none;
`;
document.body.appendChild(cursorEl);
function onPointerMove (e) {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => {
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
cursorEl.style.left = clientX + 'px';
cursorEl.style.top = clientY + 'px';
rafPending = false;
});
}
function onPointerLeave () {
cursorEl.style.opacity = '0';
}
function onPointerEnter () {
cursorEl.style.opacity = '1';
}
document.addEventListener('mousemove', onPointerMove, { passive: true });
document.addEventListener('touchmove', onPointerMove, { passive: true });
document.addEventListener('mouseleave', onPointerLeave);
document.addEventListener('mouseenter', onPointerEnter);
// Hide on idle after 4 s
let idleTimer;
function resetIdle () {
clearTimeout(idleTimer);
cursorEl.style.opacity = '1';
idleTimer = setTimeout(() => {
cursorEl.style.opacity = '0';
}, 4000);
}
document.addEventListener('mousemove', resetIdle, { passive: true });
resetIdle();
}
/* ── Boot ── */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
function boot () {
initScrollReveal();
initCursorGlow();
}
})();