Files
dangrubb.net/src/scripts/ui.js
dangrubbb ecaa3bbf6a radiance: new home page — WebGL orb hero, DG cube loader, white dropdown menu
Replaces the Crush landing page with a single-screen hero (no scroll):
gradient sky, particle orb with corona, 'Welcome to dangrubb.net' title,
top-left DG logo and a numbered menu. Raw WebGL (no three.js), self-hosted
Instrument Sans + Geist Mono. ~10 KB of JS.
2026-10-22 20:00:53 -04:00

81 lines
3.0 KiB
JavaScript

// Loader, title reveal and menu behaviour for the home page.
export function initUI(startOrb) {
const body = document.body;
const loader = document.getElementById('loader');
const count = document.getElementById('loader-count');
const title = document.getElementById('title');
const canvas = document.getElementById('orb');
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
// --- title: split into letters (each wrapped per word so lines break cleanly)
const text = title.textContent.trim();
title.setAttribute('aria-label', text);
title.textContent = '';
let n = 0;
text.split(' ').forEach((word, wi, words) => {
const w = document.createElement('span');
w.className = 'word';
w.setAttribute('aria-hidden', 'true');
for (const ch of word) {
const mask = document.createElement('span');
mask.className = 'ch';
const inner = document.createElement('span');
inner.textContent = ch;
inner.style.transitionDelay = `${0.04 * n++}s`;
mask.appendChild(inner);
w.appendChild(mask);
}
title.appendChild(w);
if (wi < words.length - 1) title.appendChild(document.createTextNode(' '));
});
// --- loader: counts up while fonts and the first orb frame get ready
let orbReady = false;
let fontsReady = false;
let shown = 0;
const started = performance.now();
const minimum = reduced ? 0 : 1100;
const webgl = startOrb(canvas, { onReady: () => { orbReady = true; } });
if (!webgl) { document.documentElement.classList.add('no-webgl'); orbReady = true; }
(document.fonts ? document.fonts.ready : Promise.resolve()).then(() => { fontsReady = true; });
function tick(now) {
const elapsed = now - started;
const real = (orbReady ? 0.5 : 0.1) + (fontsReady ? 0.5 : 0.15);
const timed = Math.min(1, elapsed / (minimum || 1));
const target = Math.min(real, timed) * 100;
shown += (target - shown) * 0.2;
if (target - shown < 0.5) shown = target;
count.textContent = String(Math.round(shown)).padStart(3, '0') + '%';
if (shown >= 99.5 && orbReady && fontsReady && elapsed >= minimum) return done();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
function done() {
count.textContent = '100%';
body.classList.remove('is-loading');
body.classList.add('is-ready');
setTimeout(() => loader.remove(), 900);
}
// --- menu
const menu = document.getElementById('menu');
const btn = document.getElementById('menu-btn');
const panel = document.getElementById('menu-panel');
function setOpen(open) {
menu.classList.toggle('is-open', open);
btn.setAttribute('aria-expanded', String(open));
panel.toggleAttribute('inert', !open);
}
setOpen(false);
btn.addEventListener('click', () => setOpen(!menu.classList.contains('is-open')));
addEventListener('keydown', (e) => { if (e.key === 'Escape') { setOpen(false); btn.focus(); } });
document.addEventListener('pointerdown', (e) => {
if (menu.classList.contains('is-open') && !menu.contains(e.target)) setOpen(false);
});
}