Game — Loading Screen (tips + progress)
A full-screen game loading screen with a layered CSS-drawn key-art backdrop — violet moon, silhouetted citadel spires with flickering windows, rising embers, and fog — over a dark neon HUD. Includes a zone lore panel with stat chips, a rotating gameplay TIP line, an animated spinner glyph, and a determinate progress bar with percentage and stage labels. Vanilla JS simulates a variable-speed load with random hitches, then reveals a pulsing press-any-key prompt; a replay button restarts the sequence.
MCP
Code
:root {
--bg: #0a0b10;
--bg-2: #12131c;
--panel: #171926;
--panel-2: #1f2233;
--text: #e7e9f3;
--muted: #9aa0bf;
--line: rgba(231, 233, 243, 0.10);
--line-2: rgba(231, 233, 243, 0.18);
--accent: #00e5ff;
--accent-2: #7c4dff;
--accent-3: #ff3d71;
--success: #36e27a;
--warn: #ffc857;
--danger: #ff4d4d;
--glow: 0 0 18px rgba(0, 229, 255, 0.45);
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--font-display: "Orbitron", sans-serif;
--font-body: "Inter", sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: none;
}
button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: var(--r-sm);
}
/* ============ Layout ============ */
.loading-screen {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
padding: clamp(16px, 3vw, 40px);
gap: 24px;
}
/* ============ CSS key art ============ */
.keyart {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
.keyart__sky {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 90% 60% at 70% 18%, rgba(124, 77, 255, 0.22), transparent 60%),
radial-gradient(ellipse 70% 50% at 30% 30%, rgba(0, 229, 255, 0.10), transparent 65%),
linear-gradient(180deg, #0b0d18 0%, #11101f 45%, #1a1226 75%, #0a0b10 100%);
}
.keyart__moon {
position: absolute;
top: 9%;
right: 14%;
width: clamp(90px, 12vw, 160px);
height: clamp(90px, 12vw, 160px);
border-radius: 50%;
background: radial-gradient(circle at 38% 34%, #f2ecff 0%, #b9a8e8 45%, #6f5aa8 100%);
box-shadow:
0 0 60px rgba(186, 160, 255, 0.45),
0 0 140px rgba(124, 77, 255, 0.35);
opacity: 0.9;
}
.keyart__moon::after {
content: "";
position: absolute;
inset: -18%;
border-radius: 50%;
border: 1px solid rgba(186, 160, 255, 0.25);
}
.keyart__ridge {
position: absolute;
left: -5%;
right: -5%;
height: 46%;
bottom: -4%;
}
.keyart__ridge--far {
background: #181530;
clip-path: polygon(0 62%, 9% 48%, 18% 58%, 30% 38%, 41% 55%, 55% 33%, 66% 52%, 78% 40%, 90% 56%, 100% 44%, 100% 100%, 0 100%);
opacity: 0.85;
}
.keyart__ridge--mid {
background: #120f24;
clip-path: polygon(0 70%, 12% 52%, 24% 66%, 38% 46%, 52% 64%, 64% 44%, 79% 62%, 91% 50%, 100% 60%, 100% 100%, 0 100%);
}
.keyart__ridge--near {
background: #0a0912;
height: 26%;
clip-path: polygon(0 60%, 14% 44%, 27% 58%, 45% 40%, 60% 56%, 76% 42%, 88% 54%, 100% 46%, 100% 100%, 0 100%);
}
.keyart__citadel {
position: absolute;
bottom: 18%;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: flex-end;
gap: clamp(8px, 1.4vw, 18px);
filter: drop-shadow(0 0 30px rgba(255, 61, 113, 0.25));
}
.spire {
display: block;
width: clamp(18px, 3vw, 40px);
background: linear-gradient(180deg, #241b3a 0%, #0e0a1c 100%);
clip-path: polygon(50% 0, 100% 12%, 100% 100%, 0 100%, 0 12%);
position: relative;
}
.spire::after {
content: "";
position: absolute;
top: 18%;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent-3);
box-shadow: 0 0 10px rgba(255, 61, 113, 0.9), 0 0 22px rgba(255, 61, 113, 0.6);
animation: window-flicker 3.2s ease-in-out infinite;
}
.spire--1 { height: clamp(70px, 11vw, 150px); }
.spire--2 { height: clamp(110px, 16vw, 220px); animation-delay: 0.4s; }
.spire--3 { height: clamp(150px, 22vw, 300px); }
.spire--4 { height: clamp(100px, 15vw, 200px); }
.spire--5 { height: clamp(65px, 10vw, 140px); }
.spire--3::after { animation-delay: 1.1s; }
.spire--4::after { animation-delay: 0.6s; }
.spire--5::after { animation-delay: 1.7s; }
@keyframes window-flicker {
0%, 100% { opacity: 1; }
45% { opacity: 0.35; }
55% { opacity: 0.9; }
60% { opacity: 0.4; }
}
.keyart__fog {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 38%;
background: linear-gradient(180deg, transparent, rgba(124, 77, 255, 0.08) 40%, rgba(10, 11, 16, 0.9));
}
.keyart__embers i {
position: absolute;
bottom: -10px;
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--accent-3);
box-shadow: 0 0 8px rgba(255, 61, 113, 0.8);
opacity: 0;
animation: ember-rise 9s linear infinite;
}
.keyart__embers i:nth-child(1) { left: 6%; animation-delay: 0s; animation-duration: 10s; }
.keyart__embers i:nth-child(2) { left: 14%; animation-delay: 2.2s; animation-duration: 8s; }
.keyart__embers i:nth-child(3) { left: 23%; animation-delay: 4.8s; animation-duration: 11s; }
.keyart__embers i:nth-child(4) { left: 32%; animation-delay: 1.1s; animation-duration: 9s; background: var(--accent); box-shadow: 0 0 8px rgba(0,229,255,0.8); }
.keyart__embers i:nth-child(5) { left: 41%; animation-delay: 6s; animation-duration: 12s; }
.keyart__embers i:nth-child(6) { left: 50%; animation-delay: 3.4s; animation-duration: 8.5s; }
.keyart__embers i:nth-child(7) { left: 58%; animation-delay: 0.8s; animation-duration: 10.5s; background: var(--accent-2); box-shadow: 0 0 8px rgba(124,77,255,0.8); }
.keyart__embers i:nth-child(8) { left: 67%; animation-delay: 5.5s; animation-duration: 9.5s; }
.keyart__embers i:nth-child(9) { left: 75%; animation-delay: 2.9s; animation-duration: 11.5s; }
.keyart__embers i:nth-child(10) { left: 83%; animation-delay: 7.2s; animation-duration: 8s; background: var(--accent); box-shadow: 0 0 8px rgba(0,229,255,0.8); }
.keyart__embers i:nth-child(11) { left: 90%; animation-delay: 4.1s; animation-duration: 10s; }
.keyart__embers i:nth-child(12) { left: 96%; animation-delay: 1.7s; animation-duration: 12.5s; }
@keyframes ember-rise {
0% { transform: translateY(0) translateX(0); opacity: 0; }
8% { opacity: 0.9; }
60% { opacity: 0.6; }
100% { transform: translateY(-78vh) translateX(24px); opacity: 0; }
}
.keyart__scanlines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(180deg, transparent 0 3px, rgba(0, 0, 0, 0.12) 3px 4px);
opacity: 0.5;
}
.keyart__vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 110% 90% at 50% 40%, transparent 50%, rgba(6, 7, 12, 0.85) 100%);
}
/* ============ Top bar ============ */
.topbar {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
.topbar__brand {
display: flex;
align-items: center;
gap: 14px;
}
.topbar__logo {
width: 44px;
height: 44px;
background: linear-gradient(145deg, var(--accent-2), var(--accent));
clip-path: polygon(50% 0, 100% 27%, 100% 73%, 50% 100%, 0 73%, 0 27%);
box-shadow: var(--glow);
animation: logo-pulse 2.8s ease-in-out infinite;
}
@keyframes logo-pulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.35); }
}
.topbar__title {
font-family: var(--font-display);
font-weight: 900;
font-size: 18px;
letter-spacing: 0.22em;
text-shadow: 0 0 14px rgba(0, 229, 255, 0.35);
}
.topbar__studio {
font-size: 12px;
font-weight: 600;
color: var(--muted);
letter-spacing: 0.14em;
text-transform: uppercase;
}
.topbar__meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.chip {
font-family: var(--font-display);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
padding: 8px 14px;
background: rgba(23, 25, 38, 0.8);
border: 1px solid var(--line-2);
clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
backdrop-filter: blur(4px);
}
.chip--zone {
color: var(--accent);
border-color: rgba(0, 229, 255, 0.4);
}
.chip--build {
color: var(--muted);
}
/* ============ Lore panel ============ */
.lore {
position: relative;
z-index: 2;
max-width: 560px;
background: linear-gradient(160deg, rgba(23, 25, 38, 0.88), rgba(18, 19, 28, 0.82));
border: 1px solid var(--line-2);
border-left: 3px solid var(--accent-2);
border-radius: var(--r-lg);
padding: clamp(18px, 2.5vw, 28px);
backdrop-filter: blur(6px);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(231, 233, 243, 0.06);
animation: lore-in 0.9s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes lore-in {
from { opacity: 0; transform: translateY(18px); }
to { opacity: 1; transform: translateY(0); }
}
.lore__zone {
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(22px, 3.4vw, 32px);
letter-spacing: 0.1em;
text-transform: uppercase;
background: linear-gradient(90deg, var(--text), var(--accent));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
}
.lore__blurb {
color: var(--muted);
font-size: 15px;
margin-bottom: 18px;
}
.lore__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.stat {
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 2px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.stat:hover {
border-color: rgba(0, 229, 255, 0.45);
box-shadow: 0 0 14px rgba(0, 229, 255, 0.18);
transform: translateY(-2px);
}
.stat__label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
}
.stat__value {
font-family: var(--font-display);
font-weight: 700;
font-size: 14px;
letter-spacing: 0.06em;
}
.stat__value--danger { color: var(--accent-3); text-shadow: 0 0 10px rgba(255, 61, 113, 0.45); }
.stat__value--accent { color: var(--accent); text-shadow: 0 0 10px rgba(0, 229, 255, 0.45); }
/* ============ Bottom HUD ============ */
.hud {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 18px;
}
.hud__tip {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 760px;
}
.hud__tip-tag {
flex-shrink: 0;
font-family: var(--font-display);
font-weight: 900;
font-size: 11px;
letter-spacing: 0.2em;
color: #061015;
background: var(--accent);
padding: 6px 12px;
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
box-shadow: var(--glow);
}
.hud__tip-text {
font-size: 14px;
font-weight: 500;
color: var(--text);
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.7);
transition: opacity 0.35s ease, transform 0.35s ease;
}
.hud__tip-text.is-swapping {
opacity: 0;
transform: translateY(6px);
}
.hud__progress-row {
display: flex;
align-items: center;
gap: 18px;
}
/* Loader glyph */
.loader-glyph {
position: relative;
width: 46px;
height: 46px;
flex-shrink: 0;
}
.loader-glyph__ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid rgba(0, 229, 255, 0.18);
border-top-color: var(--accent);
border-right-color: var(--accent-2);
animation: glyph-spin 1.1s linear infinite;
box-shadow: 0 0 16px rgba(0, 229, 255, 0.25);
}
.loader-glyph__core {
position: absolute;
inset: 14px;
background: linear-gradient(145deg, var(--accent), var(--accent-2));
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
animation: glyph-pulse 1.4s ease-in-out infinite;
}
@keyframes glyph-spin {
to { transform: rotate(360deg); }
}
@keyframes glyph-pulse {
0%, 100% { opacity: 0.55; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1); }
}
.loader-glyph.is-done .loader-glyph__ring {
animation: none;
border-color: var(--success);
box-shadow: 0 0 16px rgba(54, 226, 122, 0.45);
}
.loader-glyph.is-done .loader-glyph__core {
animation: none;
opacity: 1;
background: var(--success);
}
/* Progress bar */
.progress {
flex: 1;
min-width: 0;
}
.progress__track {
position: relative;
height: 16px;
background: rgba(23, 25, 38, 0.9);
border: 1px solid var(--line-2);
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
overflow: hidden;
}
.progress__fill {
position: absolute;
inset: 2px auto 2px 2px;
width: 0%;
background: linear-gradient(90deg, var(--accent-2), var(--accent));
clip-path: polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%);
box-shadow: 0 0 16px rgba(0, 229, 255, 0.55);
transition: width 0.18s ease-out;
}
.progress.is-done .progress__fill {
background: linear-gradient(90deg, var(--accent), var(--success));
box-shadow: 0 0 18px rgba(54, 226, 122, 0.55);
}
.progress__sheen {
position: absolute;
inset: 0;
background: linear-gradient(105deg, transparent 30%, rgba(231, 233, 243, 0.22) 50%, transparent 70%);
transform: translateX(-100%);
animation: sheen-slide 2.2s ease-in-out infinite;
}
@keyframes sheen-slide {
to { transform: translateX(100%); }
}
.progress__meta {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-top: 8px;
gap: 12px;
}
.progress__status {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.progress__pct {
font-family: var(--font-display);
font-weight: 700;
font-size: 20px;
color: var(--accent);
text-shadow: 0 0 12px rgba(0, 229, 255, 0.5);
font-variant-numeric: tabular-nums;
}
.progress.is-done .progress__pct {
color: var(--success);
text-shadow: 0 0 12px rgba(54, 226, 122, 0.5);
}
/* Continue prompt */
.hud__continue {
display: flex;
justify-content: center;
}
.continue-btn {
display: inline-flex;
align-items: center;
gap: 12px;
font-family: var(--font-display);
font-weight: 700;
font-size: 15px;
letter-spacing: 0.18em;
color: var(--text);
background: linear-gradient(160deg, rgba(31, 34, 51, 0.95), rgba(23, 25, 38, 0.95));
border: 1px solid rgba(0, 229, 255, 0.5);
padding: 14px 28px;
clip-path: polygon(14px 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
box-shadow: var(--glow);
animation: continue-pulse 1.6s ease-in-out infinite, lore-in 0.5s ease both;
transition: transform 0.15s ease, filter 0.15s ease;
}
.continue-btn:hover {
filter: brightness(1.25);
transform: translateY(-2px);
}
.continue-btn:active {
transform: translateY(0) scale(0.98);
}
.continue-btn__key {
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
font-size: 14px;
background: var(--accent);
color: #061015;
border-radius: var(--r-sm);
box-shadow: 0 0 10px rgba(0, 229, 255, 0.6);
}
@keyframes continue-pulse {
0%, 100% { box-shadow: 0 0 14px rgba(0, 229, 255, 0.35); }
50% { box-shadow: 0 0 26px rgba(0, 229, 255, 0.65); }
}
/* Restart button */
.restart-btn {
position: absolute;
top: clamp(16px, 3vw, 40px);
right: clamp(16px, 3vw, 40px);
z-index: 3;
font-family: var(--font-display);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
color: var(--muted);
background: rgba(23, 25, 38, 0.7);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 8px 12px;
transition: color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
/* sits visually under topbar meta on wide screens; offset below */
transform: translateY(52px);
}
.restart-btn:hover {
color: var(--accent);
border-color: rgba(0, 229, 255, 0.5);
box-shadow: 0 0 12px rgba(0, 229, 255, 0.25);
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 16px);
z-index: 10;
background: var(--panel-2);
border: 1px solid rgba(0, 229, 255, 0.45);
color: var(--text);
font-size: 13px;
font-weight: 600;
padding: 10px 18px;
border-radius: var(--r-md);
box-shadow: var(--glow);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* Flash on key press */
.loading-screen.is-launching::after {
content: "";
position: absolute;
inset: 0;
z-index: 9;
background: var(--text);
animation: launch-flash 0.6s ease-out forwards;
pointer-events: none;
}
@keyframes launch-flash {
0% { opacity: 0; }
25% { opacity: 0.85; }
100% { opacity: 0; }
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* ============ Responsive ============ */
@media (max-width: 760px) {
.lore__stats {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 520px) {
.loading-screen {
padding: 14px;
gap: 18px;
}
.topbar {
flex-direction: column;
}
.topbar__title {
font-size: 15px;
}
.chip {
font-size: 10px;
padding: 6px 10px;
}
.lore {
max-width: 100%;
padding: 16px;
}
.lore__stats {
grid-template-columns: 1fr;
}
.hud__progress-row {
gap: 12px;
}
.loader-glyph {
width: 36px;
height: 36px;
}
.loader-glyph__core {
inset: 11px;
}
.progress__pct {
font-size: 16px;
}
.continue-btn {
font-size: 12px;
padding: 12px 18px;
letter-spacing: 0.12em;
}
.restart-btn {
transform: none;
position: static;
align-self: flex-end;
order: -1;
}
.keyart__moon {
right: 6%;
}
}(() => {
"use strict";
// ---------- Elements ----------
const screen = document.getElementById("loadingScreen");
const fill = document.getElementById("progressFill");
const pctEl = document.getElementById("progressPct");
const bar = document.getElementById("progressBar");
const statusEl = document.getElementById("progressStatus");
const tipText = document.getElementById("tipText");
const continuePrompt = document.getElementById("continuePrompt");
const continueBtn = document.getElementById("continueBtn");
const restartBtn = document.getElementById("restartBtn");
const glyph = document.getElementById("loaderGlyph");
const toastEl = document.getElementById("toast");
// ---------- Data ----------
const TIPS = [
"Parrying just before impact refunds 30% stamina and staggers Hollow enemies.",
"Ember shards glow brighter near hidden walls. Follow the light.",
"The Hollow Court is weak to Voltcraft. Socket a Storm Rune before the gates.",
"Sprinting drains Resolve in Nightmare tier — pace your approach to the Citadel.",
"You can re-spec at any Vanguard bonfire for 200 ash marks.",
"Bell Wardens telegraph their slam twice. The third toll is the real one.",
"Co-op partners share ember pickups in Act III. Be generous — or don't.",
"Holding the dodge key performs a phase-step through thin projectiles.",
];
const STATUS_STAGES = [
[0, "Initializing world state…"],
[12, "Streaming terrain — Ashen Reach…"],
[28, "Decompressing citadel geometry…"],
[45, "Loading Hollow Court AI behaviors…"],
[62, "Compiling shaders (gloomlight pass)…"],
[78, "Spawning entities + loot tables…"],
[90, "Syncing Vanguard profile…"],
[97, "Finalizing…"],
];
// ---------- Toast ----------
let toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("is-visible"), 2200);
}
// ---------- Tips rotation ----------
let tipIndex = 0;
let tipTimer = null;
function cycleTip() {
tipText.classList.add("is-swapping");
setTimeout(() => {
tipIndex = (tipIndex + 1) % TIPS.length;
tipText.textContent = TIPS[tipIndex];
tipText.classList.remove("is-swapping");
}, 350);
}
function startTips() {
stopTips();
tipTimer = setInterval(cycleTip, 4200);
}
function stopTips() {
if (tipTimer) clearInterval(tipTimer);
tipTimer = null;
}
// ---------- Load simulation ----------
let progress = 0;
let rafId = null;
let hitchUntil = 0;
let done = false;
function statusFor(p) {
let label = STATUS_STAGES[0][1];
for (const [threshold, text] of STATUS_STAGES) {
if (p >= threshold) label = text;
}
return label;
}
function render() {
const p = Math.min(100, Math.floor(progress));
fill.style.width = p + "%";
pctEl.textContent = p + "%";
bar.setAttribute("aria-valuenow", String(p));
statusEl.textContent = done ? "Load complete" : statusFor(p);
}
function tick(now) {
if (done) return;
if (now >= hitchUntil) {
// Variable speed: fast early, slow near milestones, with random hitches.
const remaining = 100 - progress;
let speed = 0.08 + Math.random() * 0.5 + remaining * 0.004;
// Slow crawl in the last stretch for drama.
if (progress > 88) speed *= 0.3;
progress += speed;
// Occasional hitch: freeze for 250–900ms (more likely mid-load).
if (Math.random() < 0.025 && progress < 94) {
hitchUntil = now + 250 + Math.random() * 650;
}
}
if (progress >= 100) {
progress = 100;
finishLoad();
}
render();
if (!done) rafId = requestAnimationFrame(tick);
}
function finishLoad() {
done = true;
render();
stopTips();
glyph.classList.add("is-done");
document.querySelector(".progress").classList.add("is-done");
continuePrompt.hidden = false;
continueBtn.focus();
document.addEventListener("keydown", onAnyKey);
toast("Load complete — press any key");
}
function onAnyKey(e) {
// Ignore modifier-only presses and let the restart button keep working.
if (["Shift", "Control", "Alt", "Meta"].includes(e.key)) return;
launch();
}
function launch() {
document.removeEventListener("keydown", onAnyKey);
screen.classList.add("is-launching");
toast("Entering The Ashen Citadel…");
setTimeout(() => screen.classList.remove("is-launching"), 700);
}
// ---------- Restart ----------
function restart() {
cancelAnimationFrame(rafId);
document.removeEventListener("keydown", onAnyKey);
screen.classList.remove("is-launching");
progress = 0;
done = false;
hitchUntil = 0;
glyph.classList.remove("is-done");
document.querySelector(".progress").classList.remove("is-done");
continuePrompt.hidden = true;
render();
startTips();
rafId = requestAnimationFrame(tick);
toast("Replaying load sequence");
}
// ---------- Wire up ----------
continueBtn.addEventListener("click", launch);
restartBtn.addEventListener("click", (e) => {
e.stopPropagation();
restart();
});
// ---------- Boot ----------
render();
startTips();
rafId = requestAnimationFrame(tick);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hollow Reign — Loading</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="loading-screen" id="loadingScreen" aria-label="Game loading screen">
<!-- CSS-drawn key art backdrop -->
<div class="keyart" aria-hidden="true">
<div class="keyart__sky"></div>
<div class="keyart__moon"></div>
<div class="keyart__ridge keyart__ridge--far"></div>
<div class="keyart__ridge keyart__ridge--mid"></div>
<div class="keyart__citadel">
<span class="spire spire--1"></span>
<span class="spire spire--2"></span>
<span class="spire spire--3"></span>
<span class="spire spire--4"></span>
<span class="spire spire--5"></span>
</div>
<div class="keyart__ridge keyart__ridge--near"></div>
<div class="keyart__fog"></div>
<div class="keyart__embers">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<div class="keyart__scanlines"></div>
<div class="keyart__vignette"></div>
</div>
<!-- Top bar -->
<header class="topbar">
<div class="topbar__brand">
<span class="topbar__logo" aria-hidden="true"></span>
<div>
<p class="topbar__title">HOLLOW REIGN</p>
<p class="topbar__studio">Nullforge Interactive</p>
</div>
</div>
<div class="topbar__meta">
<span class="chip chip--zone" id="zoneChip">ENTERING · THE ASHEN CITADEL</span>
<span class="chip chip--build">BUILD 1.4.2 · ACT III</span>
</div>
</header>
<!-- Lore panel -->
<section class="lore" aria-label="Zone lore">
<h1 class="lore__zone">The Ashen Citadel</h1>
<p class="lore__blurb">
Once the seat of the Hollow Kings, the Citadel now burns with a fire that gives no heat.
Its gates have not opened in three hundred years — and yet tonight, something inside is
ringing the bells. The Vanguard does not send soldiers here. It sends you.
</p>
<div class="lore__stats">
<div class="stat">
<span class="stat__label">Recommended Level</span>
<span class="stat__value">42+</span>
</div>
<div class="stat">
<span class="stat__label">Faction Control</span>
<span class="stat__value stat__value--danger">Hollow Court</span>
</div>
<div class="stat">
<span class="stat__label">World Tier</span>
<span class="stat__value stat__value--accent">Nightmare</span>
</div>
</div>
</section>
<!-- Bottom HUD: tip + progress -->
<footer class="hud">
<div class="hud__tip" id="tipBox" aria-live="polite">
<span class="hud__tip-tag" aria-hidden="true">TIP</span>
<p class="hud__tip-text" id="tipText">Parrying just before impact refunds 30% stamina and staggers Hollow enemies.</p>
</div>
<div class="hud__progress-row">
<div class="loader-glyph" id="loaderGlyph" aria-hidden="true">
<span class="loader-glyph__ring"></span>
<span class="loader-glyph__core"></span>
</div>
<div class="progress" role="progressbar" aria-label="Loading progress"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="progressBar">
<div class="progress__track">
<div class="progress__fill" id="progressFill"></div>
<div class="progress__sheen" aria-hidden="true"></div>
</div>
<div class="progress__meta">
<span class="progress__status" id="progressStatus">Initializing world state…</span>
<span class="progress__pct" id="progressPct">0%</span>
</div>
</div>
</div>
<div class="hud__continue" id="continuePrompt" hidden>
<button class="continue-btn" id="continueBtn">
<span class="continue-btn__key" aria-hidden="true">⏎</span>
PRESS ANY KEY TO CONTINUE
</button>
</div>
</footer>
<!-- Restart control -->
<button class="restart-btn" id="restartBtn" type="button" aria-label="Replay loading sequence">
⟲ Replay Load
</button>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</main>
<script src="script.js"></script>
</body>
</html>Loading Screen (tips + progress)
A full-bleed in-game loading screen for the fictional action RPG Hollow Reign by Nullforge Interactive. The backdrop is drawn entirely in CSS: a gradient night sky with a glowing violet moon, layered mountain ridges built with clip-path, a citadel of silhouetted spires with flickering ember-red windows, drifting fog, rising ember particles, scanlines, and a vignette. Over it sits a lore panel for the zone being entered (“The Ashen Citadel”) with recommended level, faction control, and world-tier stat chips.
The bottom HUD carries the loading state: a neon “TIP” tag with a gameplay hint that cross-fades to a new tip every few seconds, an animated dual-ring spinner glyph, and an angled determinate progress bar with a moving sheen, a stage-based status label (“Streaming terrain…”, “Compiling shaders…”), and an Orbitron percentage readout. The script ramps progress at variable speed with occasional realistic hitches and a slow final crawl.
At 100% the spinner and bar flip to a success state and a pulsing “Press any key to continue” prompt appears — clicking it or pressing any key fires a launch flash and a toast. A “Replay Load” button resets everything and runs the simulation again. The progressbar exposes proper role/aria-valuenow attributes, tips announce via aria-live, and all controls are keyboard-accessible with visible focus rings.
Illustrative UI only — fictional games, studios, characters, and data. Not engine integrations.