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.
This commit is contained in:
80
src/scripts/ui.js
Normal file
80
src/scripts/ui.js
Normal file
@ -0,0 +1,80 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user