Instantly share code, notes, and snippets.
Created
February 27, 2026 23:44
-
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/971c199ca6db2339c3f840b5931f4743 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" /> | |
| <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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } | |
| 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