updated cybersigilism theme

This commit is contained in:
2026-05-16 17:16:40 +02:00
parent 0ade5a7e37
commit 458ef7612d
5 changed files with 568 additions and 130 deletions
+110
View File
@@ -0,0 +1,110 @@
---
/*
* 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 JS-driven mechanics (custom sigil cursor + fading trail, scroll-entry
* databend on images) and self-disables off-theme, on touch, 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 class="cs-cursor"><span class="cs-cursor-ring"></span></div>
</div>
<script>
function initCyberFx() {
const root = document.documentElement;
if (!root.classList.contains('cybersigil')) return;
const fx = document.querySelector('.cs-fx') as HTMLElement | null;
if (!fx) return;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const finePointer = window.matchMedia('(pointer: fine)').matches;
const noHover = window.matchMedia('(hover: none)').matches;
/* ─── Custom sigil cursor + fading trail ─── */
const cursor = fx.querySelector('.cs-cursor') as HTMLElement | null;
if (cursor && finePointer && !noHover) {
root.classList.add('cs-cursor-on');
let cx = window.innerWidth / 2;
let cy = window.innerHeight / 2;
let raf = 0;
let lastTrail = 0;
const HOT = 'a,button,input,textarea,select,[role="button"],.btn,.plate,.topbar-control,.back-link,.chip';
function paint() {
raf = 0;
cursor!.style.transform = `translate3d(${cx}px, ${cy}px, 0)`;
}
window.addEventListener(
'mousemove',
(e) => {
cx = e.clientX;
cy = e.clientY;
if (!raf) raf = requestAnimationFrame(paint);
const t = e.target as Element | null;
const hot = !!(t && t.closest && t.closest(HOT));
cursor!.classList.toggle('cs-cursor--hot', hot);
if (!reduced && e.timeStamp - lastTrail > 28) {
lastTrail = e.timeStamp;
const dot = document.createElement('span');
dot.className = 'cs-trail';
dot.style.left = cx + 'px';
dot.style.top = cy + 'px';
fx!.appendChild(dot);
dot.addEventListener('animationend', () => dot.remove(), { once: true });
}
},
{ passive: true }
);
window.addEventListener('mousedown', () => cursor!.classList.add('cs-cursor--down'));
window.addEventListener('mouseup', () => cursor!.classList.remove('cs-cursor--down'));
document.addEventListener('mouseleave', () => cursor!.classList.add('cs-cursor--gone'));
document.addEventListener('mouseenter', () => cursor!.classList.remove('cs-cursor--gone'));
}
/* ─── 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));
}
}
}
initCyberFx();
// MPA back/forward restores: re-arm if needed.
window.addEventListener('pageshow', (e) => {
if (e.persisted) initCyberFx();
});
</script>