Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created February 27, 2026 23:44
Show Gist options
  • Select an option

  • Save EncodeTheCode/971c199ca6db2339c3f840b5931f4743 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/971c199ca6db2339c3f840b5931f4743 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" />
<title>PS Classic Menu — Submenu & API</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
:root{ --selected-size:200px; }
html,body{height:100%;margin:0;background:radial-gradient(circle at center,#111 0%,#000 100%);color:#eee;font-family:system-ui, -apple-system, "Segoe UI", Roboto, Arial;}
.stage{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;}
.carousel{position:relative;width:900px;height:500px;pointer-events:none;will-change:transform;}
.game{
--size:100px;
position:absolute; left:50%; top:50%;
transform:translate(-50%,-50%);
width:var(--size); height:var(--size);
display:flex; align-items:center; justify-content:center;
pointer-events:auto; cursor:pointer;
border-radius:0.35em; overflow:hidden;
border:1px solid rgba(255,255,255,0.06);
background:linear-gradient(180deg,#151515,#0b0b0f);
box-shadow:0 8px 20px rgba(0,0,0,0.65);
transition: transform 420ms cubic-bezier(.25,.8,.25,1), width 420ms cubic-bezier(.25,.8,.25,1), height 420ms cubic-bezier(.25,.8,.25,1), opacity 260ms ease, box-shadow 260ms ease, border-color 200ms ease;
}
.game img{width:100%; height:100%; object-fit:cover; display:block; border-radius:inherit; pointer-events:none;}
.game.front{ border:1px solid #fff; }
.controls{position:absolute;top:16px;left:50%;transform:translateX(-50%);color:#ddd;font-size:13px;background:rgba(255,255,255,0.02);padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.03);display:flex;gap:12px;align-items:center;pointer-events:auto;}
.arrowBtn{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.04);color:#ddd;padding:8px 10px;border-radius:8px;cursor:pointer;}
.game-title{position:absolute;bottom:86px;left:50%;transform:translateX(-50%);color:#bfbfbf;font-size:18px;letter-spacing:1.6px;text-transform:uppercase;pointer-events:none;text-align:center;width:min(900px,94vw);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:0.98;}
.hud{position:absolute;bottom:26px;width:100%;text-align:center;color:#ddd;font-size:14px;pointer-events:none;}
@media (max-width:900px){ .carousel{width:94vw;height:46vh;} .game-title{font-size:14px;bottom:70px;} }
</style>
</head>
<body>
<div class="stage">
<div class="carousel" id="carousel"></div>
<div class="controls" aria-hidden="false">
<div class="menuLabel" id="menuTitle">Main Menu</div>
<div style="display:flex;gap:10px;align-items:center;">
<button class="arrowBtn" id="btnLeft">A / ←</button>
<button class="arrowBtn" id="btnRight">D / →</button>
</div>
</div>
<div class="game-title" id="gameTitle" aria-live="polite" aria-atomic="true"></div>
<div class="hud" id="hintText">Press <strong>A</strong>/<strong>D</strong> or ←/→ to rotate, <strong>Enter</strong> to select, <strong>R</strong> to return</div>
</div>
<script>
/* ======================
MENU DATA
====================== */
const mainGames = [
{ title: 'Crash Bandicoot', color: '#b2362f', cover:'https://upload.wikimedia.org/wikipedia/en/4/44/Crash_Bandicoot_Cover.png' },
{ title: 'Spyro the Dragon', color: '#2f6fb2', cover:'https://upload.wikimedia.org/wikipedia/en/5/53/Spyro_the_Dragon.jpg' },
{ title:'Tekken 3', color:'#2fb280' },
{ title:'Final Fantasy VII', color:'#b28b2f' },
{ title:'Metal Gear Solid', color:'#8a2fb2' },
{ title:'Options', color:'#4c6fb2' }
];
const optionsGames = [
{ title:'Display Settings', color:'#3f7fb2' },
{ title:'Audio Settings', color:'#5fb27f' },
{ title:'Controls', color:'#b27f3f' },
{ title:'Network', color:'#b25f9a' },
{ title:'System Info', color:'#7f8fb2' },
{ title:'Back', color:'#4c6fb2' }
];
/* ======================
UTILITY FUNCTIONS
====================== */
function escapeForSVG(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function makeSVGData(title,color){ const safe=escapeForSVG(title); return 'data:image/svg+xml;utf8,'+encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400'><rect width='100%' height='100%' fill='${color}'/><text x='50%' y='50%' font-size='34' fill='#fff' dominant-baseline='middle' text-anchor='middle'>${safe}</text></svg>`); }
/* ======================
STATE
====================== */
const $carousel = $('#carousel');
let menuData=[], nodes=[], selected=0, currentMenu='main', menuStack=[], animating=false;
/* ======================
LAYOUT
====================== */
function getLayoutParams(){ const rect=$carousel[0].getBoundingClientRect(); return { rX: Math.min(rect.width,900)*0.42, rY: Math.min(rect.height,500)*0.34, width:rect.width, height:rect.height }; }
function angleStepFor(n){ return 2*Math.PI/n; }
function signedAngle(raw){ while(raw<=-Math.PI) raw+=2*Math.PI; while(raw>Math.PI) raw-=2*Math.PI; return raw; }
function sizeFromD(d){ return d<=0.5 ? 110-(110-80)*(d/0.5) : 80-(80-55)*((d-0.5)/0.5); }
/* ======================
BUILD MENU
====================== */
function buildMenu(menuArray){
$carousel.empty(); nodes=[];
menuArray.forEach((g,i)=>{
const el=document.createElement('div'); el.className='game';
el.dataset.index=i;
el.setAttribute('role','button'); el.setAttribute('tabindex','0');
const img=document.createElement('img'); img.alt=g.title;
img.src = g.cover ? g.cover : makeSVGData(g.title,g.color||'#444');
el.appendChild(img);
$carousel.append(el); nodes.push(el);
el.addEventListener('click', ()=>{ selected=i; handleEnterOnSelected(); });
el.addEventListener('keydown',(ev)=>{ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); handleEnterOnSelected(); }});
});
menuData=menuArray.slice();
updatePositions();
}
function updatePositions(){
const params=getLayoutParams();
const N=nodes.length;
const step=angleStepFor(N);
nodes.forEach((node,i)=>{
const raw=signedAngle((i-selected)*step);
const d=Math.abs(raw)/Math.PI;
const size=sizeFromD(d);
const x=Math.sin(raw)*params.rX;
const y=Math.cos(raw)*params.rY;
node.style.width=size+'px'; node.style.height=size+'px';
node.style.transform=`translate(-50%,-50%) translate(${x}px,${y}px) scale(${size/110})`;
node.style.zIndex=Math.round((1-d)*1000);
node.style.opacity=1;
node.classList.toggle('front', i===selected);
});
$('#gameTitle').text(menuData[selected]?.title||'');
}
/* ======================
ENTER / SUBMENU
====================== */
function handleEnterOnSelected(){
const item=menuData[selected];
if(!item) return;
const t=item.title.toLowerCase();
if(t==='options'){ openOptions(); return; }
if(t==='back'){ returnToParent(); return; }
// Placeholder for actual game select
alert(`Selected: ${item.title}`);
}
function openOptions(){
if(animating) return;
menuStack.push({ menu: currentMenu, data: menuData.slice(), selectedIndex: selected });
currentMenu='options';
buildMenu(optionsGames);
selected=0; updatePositions();
}
function returnToParent(){
if(!menuStack.length) return;
const prev=menuStack.pop();
currentMenu=prev.menu;
buildMenu(prev.data);
selected=prev.selectedIndex;
updatePositions();
}
/* ======================
NAVIGATION
====================== */
function moveLeft(){ selected=(selected-1+nodes.length)%nodes.length; updatePositions(); }
function moveRight(){ selected=(selected+1)%nodes.length; updatePositions(); }
/* ======================
KEYBOARD & BUTTONS
====================== */
$(window).on('keydown', (ev)=>{
if(ev.key==='ArrowLeft'||ev.key==='a'||ev.key==='A'){ ev.preventDefault(); moveLeft(); }
if(ev.key==='ArrowRight'||ev.key==='d'||ev.key==='D'){ ev.preventDefault(); moveRight(); }
if(ev.key==='Enter'){ ev.preventDefault(); handleEnterOnSelected(); }
if(ev.key==='r'||ev.key==='R'){ ev.preventDefault(); if(currentMenu==='options'){ returnToParent(); } }
});
$('#btnLeft').on('click', moveLeft);
$('#btnRight').on('click', moveRight);
$(window).on('resize', ()=>{ updatePositions(); });
/* ======================
INIT
====================== */
$(function(){ buildMenu(mainGames); selected=0; updatePositions(); });
/* ======================
RUNTIME API
====================== */
window._ps={
openOptions: ()=>openOptions(),
returnToParent: ()=>returnToParent(),
moveLeft: ()=>moveLeft(),
moveRight: ()=>moveRight(),
select: ()=>handleEnterOnSelected()
};
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment