Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created May 17, 2026 10:45
Show Gist options
  • Select an option

  • Save EncodeTheCode/92c6c188d39246a622f5c148fe08cc1c to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/92c6c188d39246a622f5c148fe08cc1c to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo One PS1-Style Menu</title>
<style>
:root {
--bg: #05070b;
--panel: rgba(10, 18, 30, 0.62);
--panel-2: rgba(255,255,255,0.07);
--text: #e9f0ff;
--muted: rgba(233, 240, 255, 0.58);
--accent: #89b8ff;
--accent2: #ffd36a;
--shadow: rgba(0,0,0,0.55);
--ring-radius: 230px;
--ring-item-width: 210px;
--ring-item-height: 54px;
--perspective: 1100px;
--ring-tilt: 24deg;
--belt-speed: 0.035; /* items per frame-ish; increase for faster conveyor motion */
--belt-gap: 248px;
--belt-depth: 150px;
--glow: 0 0 18px rgba(137,184,255,0.18);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at 50% 25%, rgba(80,120,180,0.18), transparent 32%),
radial-gradient(circle at 50% 70%, rgba(130,160,255,0.09), transparent 40%),
linear-gradient(180deg, #020408 0%, #05070b 52%, #020407 100%);
color: var(--text);
font-family: "Trebuchet MS", "Verdana", "Segoe UI", sans-serif;
letter-spacing: 0.02em;
}
.app {
position: relative;
width: 100%;
height: 100%;
perspective: var(--perspective);
perspective-origin: 50% 50%;
overflow: hidden;
}
.scanlines {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(255,255,255,0.028) 1px, transparent 1px);
background-size: 100% 4px;
opacity: 0.18;
pointer-events: none;
mix-blend-mode: screen;
}
.vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(circle at center, transparent 0 55%, rgba(0,0,0,0.42) 100%);
}
.hud {
position: absolute;
left: 3vw;
top: 3vh;
z-index: 5;
color: var(--muted);
font-size: 12px;
line-height: 1.45;
text-transform: uppercase;
letter-spacing: 0.12em;
text-shadow: 0 0 10px rgba(0,0,0,0.65);
}
.title {
margin-top: 8px;
font-size: clamp(28px, 4vw, 54px);
line-height: 0.95;
color: var(--text);
font-weight: 700;
text-shadow: 0 0 18px rgba(137,184,255,0.28), 0 2px 0 rgba(0,0,0,0.4);
}
.subtitle {
margin-top: 10px;
max-width: 520px;
color: rgba(233,240,255,0.72);
font-size: 13px;
text-transform: none;
letter-spacing: 0.03em;
}
.scene {
position: absolute;
inset: 0;
display: grid;
place-items: center;
transform-style: preserve-3d;
}
.center-stage {
position: relative;
width: min(88vw, 1200px);
height: min(74vh, 760px);
transform-style: preserve-3d;
transform: translateY(10px) rotateX(var(--ring-tilt));
}
.menu-track {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: translateZ(0);
}
.belt {
position: absolute;
left: 50%;
top: 48%;
width: 1px;
height: 1px;
transform-style: preserve-3d;
transform: translateZ(0);
}
.menu-item {
position: absolute;
left: 50%;
top: 50%;
width: var(--ring-item-width);
height: var(--ring-item-height);
margin-left: calc(var(--ring-item-width) / -2);
margin-top: calc(var(--ring-item-height) / -2);
display: flex;
align-items: center;
justify-content: center;
transform-style: preserve-3d;
backface-visibility: hidden;
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.12);
background:
linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0.03)),
linear-gradient(90deg, rgba(22,32,48,0.80), rgba(10,16,28,0.54));
box-shadow: 0 14px 26px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.1), var(--glow);
color: rgba(233,240,255,0.75);
font-size: 18px;
font-weight: 700;
text-shadow: 0 1px 0 rgba(0,0,0,0.55);
overflow: hidden;
transition:
transform 260ms cubic-bezier(.2,.82,.2,1),
opacity 220ms ease,
filter 220ms ease,
background 220ms ease,
color 220ms ease,
border-color 220ms ease;
will-change: transform, opacity;
}
.menu-item::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
opacity: 0.35;
transform: translateX(-120%);
transition: transform 700ms ease;
}
.menu-item.active {
color: white;
border-color: rgba(137,184,255,0.55);
background:
linear-gradient(180deg, rgba(137,184,255,0.26), rgba(255,255,255,0.04)),
linear-gradient(90deg, rgba(29,51,88,0.94), rgba(10,16,28,0.66));
box-shadow:
0 18px 34px rgba(0,0,0,0.35),
0 0 26px rgba(137,184,255,0.34),
inset 0 1px 0 rgba(255,255,255,0.18);
}
.menu-item.active::before {
transform: translateX(120%);
}
.menu-item.hidden {
opacity: 0;
filter: blur(6px);
pointer-events: none;
}
.menu-item .divider {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: 0 8px;
font-size: 18px;
color: rgba(255,255,255,0.72);
text-shadow: 0 0 6px rgba(255,255,255,0.15);
flex: 0 0 auto;
transform: translateY(-1px);
}
.menu-item .divider img {
width: 15px;
height: 15px;
object-fit: contain;
image-rendering: pixelated;
filter: drop-shadow(0 0 3px rgba(255,255,255,0.15));
}
.center-panel {
position: absolute;
left: 50%;
top: 62%;
transform: translate(-50%, -50%) translateZ(130px);
width: min(680px, 82vw);
padding: 18px 22px;
background: linear-gradient(180deg, rgba(4,10,18,0.68), rgba(4,10,18,0.34));
border: 1px solid rgba(255,255,255,0.10);
border-radius: 20px;
box-shadow: 0 18px 44px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
backdrop-filter: blur(6px);
text-align: center;
}
.current-label {
font-size: clamp(24px, 3vw, 40px);
font-weight: 800;
color: #f2f7ff;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 8px;
text-shadow: 0 0 18px rgba(137,184,255,0.24);
}
.current-subtitle {
color: rgba(233,240,255,0.68);
font-size: 13px;
letter-spacing: 0.04em;
text-transform: none;
margin-bottom: 18px;
}
.submenu-wrap {
position: relative;
width: 100%;
min-height: 160px;
transform-style: preserve-3d;
perspective: 900px;
}
.submenu-ring {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transform: rotateX(18deg);
}
.submenu-item {
position: absolute;
left: 50%;
top: 50%;
width: 140px;
height: 34px;
margin-left: -70px;
margin-top: -17px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.09);
background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.02));
color: rgba(233,240,255,0.82);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
transform-style: preserve-3d;
transition: opacity 220ms ease, transform 260ms cubic-bezier(.2,.82,.2,1), background 220ms ease, color 220ms ease;
opacity: 0;
}
.submenu-item.visible {
opacity: 1;
}
.submenu-item.active {
color: #ffffff;
background: linear-gradient(180deg, rgba(137,184,255,0.18), rgba(255,255,255,0.04));
box-shadow: 0 0 18px rgba(137,184,255,0.15);
border-color: rgba(137,184,255,0.3);
}
.footer {
position: absolute;
left: 3vw;
bottom: 3vh;
z-index: 5;
color: rgba(233,240,255,0.50);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
line-height: 1.55;
}
.footer strong {
color: rgba(233,240,255,0.82);
font-weight: 700;
}
.pulse {
animation: pulse 2.8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
@media (max-width: 760px) {
:root {
--ring-item-width: 150px;
--ring-item-height: 46px;
--perspective: 900px;
--belt-gap: 178px;
--belt-depth: 110px;
}
.center-panel { top: 64%; }
.menu-item { font-size: 15px; border-radius: 14px; }
.submenu-item { width: 110px; margin-left: -55px; }
}
</style>
</head>
<body>
<div class="app" id="app" tabindex="0" aria-label="PlayStation-style menu">
<div class="scanlines"></div>
<div class="vignette"></div>
<div class="hud">
DEMO ONE-INSPIRED INTERFACE<br>
ARROW LEFT / RIGHT TO SWITCH MENU GROUPS<br>
ARROW UP / DOWN TO SWITCH ITEMS IN THE CURRENT GROUP
</div>
<div class="scene">
<div class="center-stage">
<div class="menu-track">
<div class="belt" id="belt"></div>
</div>
<div class="center-panel pulse">
<div class="current-label" id="currentLabel">Games</div>
<div class="current-subtitle" id="currentSubtitle">Use the d-pad to move through the menu belt.</div>
<div class="submenu-wrap">
<div class="submenu-ring" id="submenuRing"></div>
</div>
</div>
</div>
</div>
<div class="footer">
<strong>Hint:</strong> replace divider placeholders with image URLs in <code>dividerAssets</code>.
</div>
</div>
<script>
const dividerAssets = [null, null, null];
const menuData = [
{
title: "Games",
subtitle: "Browse game-related demo entries.",
items: ["Start", "Load", "Options", "Credits", "Memory Card"]
},
{
title: "Videos",
subtitle: "Select a clip from the demo disc video library.",
items: ["Trailer One", "Trailer Two", "Play All", "Scene Select"]
},
{
title: "Reviews",
subtitle: "Read magazine-style review entries.",
items: ["Latest", "Top Rated", "Editor Picks", "Archive"]
}
];
const belt = document.getElementById('belt');
const submenuRing = document.getElementById('submenuRing');
const currentLabel = document.getElementById('currentLabel');
const currentSubtitle = document.getElementById('currentSubtitle');
const app = document.getElementById('app');
const visibleNeighbors = 4;
const maxMainItems = 9;
const maxSubItems = 8;
let activeMenuIndex = 0;
let activeSubIndex = 0;
let beltOffset = 0;
let lastTime = performance.now();
let manualTarget = 0;
function getDividerMarkup(index) {
const asset = dividerAssets[index % dividerAssets.length];
if (asset) {
return `<span class="divider"><img src="${asset}" alt="*" /></span>`;
}
return `<span class="divider" aria-hidden="true">*</span>`;
}
function buildBelt() {
belt.innerHTML = '';
const activeTitle = menuData[activeMenuIndex].title;
const spacing = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--belt-gap')) || 248;
const depth = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--belt-depth')) || 150;
const count = maxMainItems;
for (let i = 0; i < count; i++) {
const item = document.createElement('div');
item.className = 'menu-item';
item.innerHTML = `<span class="label">${activeTitle}</span>${getDividerMarkup(i)}<span class="label">${activeTitle}</span>`;
const offset = i - Math.floor(count / 2) - beltOffset;
const x = offset * spacing;
const z = -Math.abs(offset) * depth;
const scale = Math.max(0.72, 1 - Math.abs(offset) * 0.11);
const opacity = Math.max(0, 1 - Math.abs(offset) * 0.18);
const blur = Math.max(0, Math.abs(offset) * 0.4);
item.style.transform = `translate3d(${x}px, 0px, ${z}px) scale(${scale})`;
item.style.opacity = opacity.toFixed(2);
item.style.filter = `blur(${blur}px)`;
if (Math.abs(offset) < 0.5) {
item.classList.add('active');
item.style.opacity = '1';
item.style.filter = 'blur(0px)';
}
// Hide far-away entries, matching the Demo One feel.
if (Math.abs(offset) > visibleNeighbors) {
item.classList.add('hidden');
}
belt.appendChild(item);
}
}
function buildSubmenu() {
submenuRing.innerHTML = '';
const current = menuData[activeMenuIndex];
const count = Math.max(maxSubItems, current.items.length);
const radius = 115;
for (let i = 0; i < count; i++) {
const itemText = current.items[i % current.items.length];
const item = document.createElement('div');
item.className = 'submenu-item visible';
item.textContent = itemText;
const angle = (360 / count) * i;
item.style.transform = `rotateY(${angle}deg) translateZ(${radius}px) rotateY(${-angle}deg)`;
if (i % current.items.length === activeSubIndex) {
item.classList.add('active');
}
submenuRing.appendChild(item);
}
}
function render() {
const current = menuData[activeMenuIndex];
currentLabel.textContent = current.title;
currentSubtitle.textContent = current.subtitle;
buildBelt();
buildSubmenu();
}
function moveMenu(delta) {
activeMenuIndex = (activeMenuIndex + delta + menuData.length) % menuData.length;
activeSubIndex = 0;
manualTarget = 0;
render();
}
function moveSubmenu(delta) {
const current = menuData[activeMenuIndex];
activeSubIndex = (activeSubIndex + delta + current.items.length) % current.items.length;
render();
}
function tick(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
// Auto-spin left at a constant rate.
beltOffset -= parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--belt-speed')) * dt * 60;
// Keep the active item centered when the user changes menus.
beltOffset += (manualTarget - beltOffset) * Math.min(1, dt * 12);
// Wrap to avoid large numbers.
if (beltOffset > 1000 || beltOffset < -1000) beltOffset = 0;
buildBelt();
requestAnimationFrame(tick);
}
document.addEventListener('keydown', (e) => {
const key = e.key;
if (key === 'ArrowLeft') {
e.preventDefault();
moveMenu(-1);
} else if (key === 'ArrowRight') {
e.preventDefault();
moveMenu(1);
} else if (key === 'ArrowUp') {
e.preventDefault();
moveSubmenu(-1);
} else if (key === 'ArrowDown') {
e.preventDefault();
moveSubmenu(1);
}
});
window.addEventListener('load', () => {
app.focus();
render();
requestAnimationFrame(tick);
});
window.addEventListener('resize', render);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment