/** * 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 or 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(); } })();