Instantly share code, notes, and snippets.
Created
May 17, 2026 10:45
-
Star
1
(1)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save EncodeTheCode/92c6c188d39246a622f5c148fe08cc1c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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