137 lines
4.6 KiB
Plaintext
137 lines
4.6 KiB
Plaintext
---
|
|
/*
|
|
* CyberFx — ambient + interactive layer for the `.cybersigil` theme.
|
|
*
|
|
* Renders an aria-hidden overlay root on every page. All visuals are CSS,
|
|
* scoped to `.cybersigil .cs-fx*` in global.css, so this is an inert,
|
|
* display:none no-op under every other theme. The bundled script only wires
|
|
* the scroll-entry databend on images and self-disables off-theme or under
|
|
* prefers-reduced-motion.
|
|
*/
|
|
---
|
|
|
|
<div class="cs-fx" aria-hidden="true">
|
|
<div class="cs-fx-halftone"></div>
|
|
<div class="cs-fx-wire"></div>
|
|
<div class="cs-fx-tear"></div>
|
|
<i class="cs-fx-corner cs-fx-corner--tl"></i>
|
|
<i class="cs-fx-corner cs-fx-corner--tr"></i>
|
|
<i class="cs-fx-corner cs-fx-corner--bl"></i>
|
|
<i class="cs-fx-corner cs-fx-corner--br"></i>
|
|
</div>
|
|
|
|
<script>
|
|
import { buildCybersigil } from '../lib/cybersigil';
|
|
|
|
let teardown: (() => void) | null = null;
|
|
|
|
function initCyberFx() {
|
|
const root = document.documentElement;
|
|
if (!root.classList.contains('cybersigil')) return;
|
|
const fx = document.querySelector<HTMLElement>('.cs-fx');
|
|
if (!fx) return;
|
|
|
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
/* ─── Generated sigil growths in the four corners ─── */
|
|
/* One sigil per page load; the four corner transforms in global.css
|
|
* (scaleX / scaleY / scale) splay it into each corner. */
|
|
const corners = document.querySelectorAll<HTMLElement>('.cs-fx-corner');
|
|
if (corners.length) {
|
|
const svg = buildCybersigil();
|
|
corners.forEach((c) => {
|
|
if (c.classList.contains('cs-fx-corner--sig')) return;
|
|
c.innerHTML = svg;
|
|
c.classList.add('cs-fx-corner--sig');
|
|
});
|
|
}
|
|
|
|
/* ─── One sigil filling the background. count:6 (was 9) — fewer branch
|
|
* nodes ⇒ far fewer perpetually-animating strokes, same silhouette. ─── */
|
|
const wire = document.querySelector<HTMLElement>('.cs-fx-wire');
|
|
if (wire && !wire.classList.contains('cs-fx-wire--sig')) {
|
|
wire.innerHTML = buildCybersigil({ count: 6 });
|
|
wire.classList.add('cs-fx-wire--sig');
|
|
}
|
|
|
|
/* ─── Scroll-entry databend on images ─── */
|
|
if (!reduced && 'IntersectionObserver' in window) {
|
|
const targets = document.querySelectorAll<HTMLElement>(
|
|
'.prose img, .prose figure img, .plate-image img'
|
|
);
|
|
if (targets.length) {
|
|
const io = new IntersectionObserver(
|
|
(entries) => {
|
|
for (const en of entries) {
|
|
if (!en.isIntersecting) continue;
|
|
const el = en.target as HTMLElement;
|
|
el.classList.remove('cs-databent');
|
|
// reflow so the animation can retrigger
|
|
void el.offsetWidth;
|
|
el.classList.add('cs-databent');
|
|
io.unobserve(el);
|
|
}
|
|
},
|
|
{ rootMargin: '0px 0px -12% 0px', threshold: 0.15 }
|
|
);
|
|
targets.forEach((t) => io.observe(t));
|
|
}
|
|
}
|
|
|
|
/* ─── Ambient: entrance fade-in + scroll recede + parallax ─── */
|
|
if (teardown) teardown();
|
|
const off: Array<() => void> = [];
|
|
let depth = 0, raf = 0;
|
|
let mx = 0, my = 0; // mouse relative (-1..1)
|
|
|
|
const apply = () => {
|
|
raf = 0;
|
|
fx.style.opacity = String(1 - 0.5 * depth);
|
|
|
|
// subtle parallax drift
|
|
root.style.setProperty('--cs-px', `${(mx * 15).toFixed(1)}px`);
|
|
root.style.setProperty('--cs-py', `${(my * 15).toFixed(1)}px`);
|
|
root.style.setProperty('--cs-cx', `${(mx * -8).toFixed(1)}px`);
|
|
root.style.setProperty('--cs-cy', `${(my * -8).toFixed(1)}px`);
|
|
};
|
|
const schedule = () => { if (!raf) raf = requestAnimationFrame(apply); };
|
|
|
|
const onScroll = () => {
|
|
const vh = window.innerHeight || 1;
|
|
depth = Math.max(0, Math.min(1, window.scrollY / vh));
|
|
schedule();
|
|
};
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
mx = (e.clientX / window.innerWidth) * 2 - 1;
|
|
my = (e.clientY / window.innerHeight) * 2 - 1;
|
|
schedule();
|
|
};
|
|
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
window.addEventListener('resize', onScroll);
|
|
window.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
off.push(() => window.removeEventListener('scroll', onScroll));
|
|
off.push(() => window.removeEventListener('resize', onScroll));
|
|
off.push(() => window.removeEventListener('mousemove', onMouseMove));
|
|
onScroll();
|
|
|
|
/* Freeze every loop while the tab is hidden — idle-battery win. */
|
|
const onVis = () => fx.classList.toggle('is-paused', document.hidden);
|
|
document.addEventListener('visibilitychange', onVis);
|
|
off.push(() => document.removeEventListener('visibilitychange', onVis));
|
|
onVis();
|
|
|
|
teardown = () => {
|
|
if (raf) cancelAnimationFrame(raf);
|
|
off.forEach((fn) => fn());
|
|
teardown = null;
|
|
};
|
|
}
|
|
|
|
initCyberFx();
|
|
// MPA back/forward restores: re-arm if needed.
|
|
window.addEventListener('pageshow', (e) => {
|
|
if (e.persisted) initCyberFx();
|
|
});
|
|
</script>
|