// Boot-up loading screen — terminal init sequence with per-character typing const { useState, useEffect, useRef } = React; function LoadingScreen({ onSettling, onDone }) { const [phase, setPhase] = useState("typing"); // typing → fading const [committed, setCommitted] = useState([]); // fully-typed past lines const [active, setActive] = useState(null); // { kind, text, typed } const [progress, setProgress] = useState(0); const brandRef = useRef(null); const sequence = [ { kind: "info", text: "booting nyxoril.dev v1.0.0", postDelay: 140 }, { kind: "ok", text: "renderer ready · gpu accelerated", postDelay: 120 }, { kind: "load", text: "loading assets/pfp.png", postDelay: 180 }, { kind: "load", text: "compiling shaders…", postDelay: 240 }, { kind: "ok", text: "shaders compiled · 6 covers ready", postDelay: 120 }, { kind: "load", text: "mounting ", postDelay: 160 }, { kind: "info", text: "engines online: Unreal · Unity · Godot · Web", postDelay: 200 }, { kind: "ok", text: "ready · welcome", postDelay: 250 } ]; // when phase flips to fading, morph the brand mark to the hero avatar useEffect(() => { if (phase !== "fading") return; onSettling && onSettling(); const morphTo = () => { const target = document.querySelector(".hero__avatar"); const brand = brandRef.current; if (!target || !brand) return; const t = target.getBoundingClientRect(); const b = brand.getBoundingClientRect(); const dx = (t.left + t.width / 2) - (b.left + b.width / 2); const dy = (t.top + t.height / 2) - (b.top + b.height / 2); const scale = t.width / b.width; brand.style.setProperty("--morph-x", `${dx}px`); brand.style.setProperty("--morph-y", `${dy}px`); brand.style.setProperty("--morph-scale", String(scale)); }; requestAnimationFrame(() => requestAnimationFrame(morphTo)); }, [phase, onSettling]); useEffect(() => { if (sessionStorage.getItem("nyx_seen_boot")) { onDone(); return; } let timer; let cancelled = false; let targetProgress = 0; let progRaf; // smooth progress tweener — eases the bar toward targetProgress every frame let displayed = 0; const tween = () => { if (cancelled) return; displayed += (targetProgress - displayed) * 0.18; if (Math.abs(targetProgress - displayed) < 0.2 && targetProgress >= 100) displayed = 100; setProgress(displayed); progRaf = requestAnimationFrame(tween); }; progRaf = requestAnimationFrame(tween); const finish = () => { if (cancelled) return; targetProgress = 100; setActive(null); timer = setTimeout(() => { if (cancelled) return; setPhase("fading"); timer = setTimeout(() => { if (cancelled) return; sessionStorage.setItem("nyx_seen_boot", "1"); onDone(); }, 1100); }, 280); }; let li = 0; // type one character of the current line at a time, then commit & advance const typeNext = () => { if (cancelled) return; if (li >= sequence.length) { finish(); return; } const step = sequence[li]; const chars = step.text.length; let ci = 0; // set initial active line (tag visible, empty text) setActive({ kind: step.kind, text: step.text, typed: "" }); const charDelay = Math.max(9, 22 - chars * 0.18); // shorter for longer lines const typeChar = () => { if (cancelled) return; ci++; setActive({ kind: step.kind, text: step.text, typed: step.text.slice(0, ci) }); if (ci < chars) { timer = setTimeout(typeChar, charDelay + (Math.random() < 0.08 ? 30 : 0)); } else { // commit, bump progress, schedule next setCommitted((c) => [...c, step]); setActive(null); li++; targetProgress = Math.round((li / sequence.length) * 100); timer = setTimeout(typeNext, step.postDelay); } }; timer = setTimeout(typeChar, 60); }; timer = setTimeout(typeNext, 260); const skip = () => { if (cancelled) return; sessionStorage.setItem("nyx_seen_boot", "1"); cancelled = true; clearTimeout(timer); cancelAnimationFrame(progRaf); setPhase("fading"); setTimeout(onDone, 900); }; window.addEventListener("keydown", skip); window.addEventListener("click", skip); return () => { cancelled = true; clearTimeout(timer); cancelAnimationFrame(progRaf); window.removeEventListener("keydown", skip); window.removeEventListener("click", skip); }; // eslint-disable-next-line }, []); const tagColor = { info: "oklch(0.85 0.18 195)", ok: "oklch(0.85 0.18 145)", load: "oklch(0.85 0.18 50)", err: "oklch(0.85 0.18 25)" }; const tagLabel = { info: "INFO", ok: "OK ", load: "LOAD", err: "ERR " }; return (
NYXORIL.DEV
cédrick picard · game developer
~ ❯ ./start.sh
{committed.filter(Boolean).map((l, i) => (
[{tagLabel[l.kind]}] {l.text}
))} {active && (
[{tagLabel[active.kind]}] {active.typed}
)} {!active && phase === "typing" && (
)}
{String(Math.round(progress)).padStart(3, "0")}% click anywhere to skip
); } window.LoadingScreen = LoadingScreen;