// 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;