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.
81 lines
3.0 KiB
JavaScript
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);
|
|
});
|
|
}
|