Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created February 27, 2026 21:00
Show Gist options
  • Select an option

  • Save EncodeTheCode/7367065d583203169a7b1698b10ddfe2 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/7367065d583203169a7b1698b10ddfe2 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PS Classic Menu — Final</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
html,body{
margin:0;
height:100%;
background:radial-gradient(circle at center,#1a1a1a 0%,#000 100%);
overflow:hidden;
font-family:Arial, sans-serif;
}
.stage{
position:relative;
width:100%;
height:100%;
display:flex;
align-items:center;
justify-content:center;
}
.carousel{
position:relative;
width:900px;
height:500px;
}
.game{
position:absolute;
left:50%;
top:50%;
transform:translate(-50%,-50%);
border-radius:0.35em;
border:1px solid rgba(255,255,255,0.15);
overflow:hidden;
transition: transform .45s cubic-bezier(.25,.8,.25,1),
width .45s cubic-bezier(.25,.8,.25,1),
height .45s cubic-bezier(.25,.8,.25,1),
box-shadow .45s cubic-bezier(.25,.8,.25,1),
opacity .3s ease;
box-shadow:0 8px 20px rgba(0,0,0,.7);
}
.game img{
width:100%;
height:100%;
object-fit:cover;
display:block;
border-radius:inherit;
}
.game.front{
border:1px solid #fff !important;
box-shadow:
0 0 20px rgba(255,255,255,.7),
0 0 60px rgba(255,255,255,.2);
}
.title{
position:absolute;
bottom:-70px;
left:50%;
transform:translateX(-50%);
width:500px;
text-align:center;
font-size:20px;
letter-spacing:2px;
text-transform:uppercase;
color:#ccc;
opacity:.9;
pointer-events:none;
text-shadow:0 0 8px rgba(255,255,255,.2);
}
</style>
</head>
<body>
<div class="stage">
<div class="carousel" id="carousel"></div>
</div>
<script>
/* ========================= */
/* PERSPECTIVE CONTROL */
/* ========================= */
let perspectiveTilt = 120; // ← adjust this value to tilt perspective
let spacing = 0.75;
const TRANSITION_DURATION = 1000;
let selected = 0;
let inSubMenu = false;
/* ========================= */
/* MENUS */
/* ========================= */
const mainMenu = [
{title:'Crash Bandicoot', color:'#b2362f', image:'images/crash.jpg'},
{title:'Spyro the Dragon', color:'#2f6fb2'},
{title:'Tekken 3', color:'#2fb280'},
{title:'Final Fantasy VII', color:'#b28b2f'},
{title:'Metal Gear Solid', color:'#8a2fb2'},
{title:'Options', color:'#4c6fb2'}
];
const optionsMenu = [
{title:'Display Settings', color:'#444'},
{title:'Sound Settings', color:'#666'},
{title:'Controller Setup', color:'#888'},
{title:'Back', color:'#222'}
];
let activeMenu = mainMenu;
const carousel = document.getElementById("carousel");
/* ========================= */
/* IMAGE FALLBACK */
/* ========================= */
function makeSVG(title,color){
return 'data:image/svg+xml;utf8,'+
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
<defs>
<linearGradient id="g" x1="0" x2="1">
<stop offset="0" stop-color="${color}"/>
<stop offset="1" stop-color="#111"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#g)"/>
<text x="50%" y="50%" font-size="28"
fill="white"
text-anchor="middle"
dominant-baseline="middle"
font-family="Arial">${title}</text>
</svg>
`);
}
/* ========================= */
/* CREATE NODES */
/* ========================= */
function createNodes(){
carousel.innerHTML = "";
activeMenu.forEach(item=>{
const div=document.createElement("div");
div.className="game";
const img=document.createElement("img");
if(item.image){
img.src=item.image;
img.onerror=function(){
this.src=makeSVG(item.title,item.color||"#444");
};
} else {
img.src=makeSVG(item.title,item.color||"#444");
}
div.appendChild(img);
carousel.appendChild(div);
});
const title=document.createElement("div");
title.className="title";
title.innerText=activeMenu[selected].title;
carousel.appendChild(title);
}
/* ========================= */
/* POSITIONING LOGIC */
/* ========================= */
function updatePositions(){
const nodes=document.querySelectorAll(".game");
const total=nodes.length;
const rect=carousel.getBoundingClientRect();
const rX=rect.width*0.45*spacing;
const rY=rect.height*0.35*spacing;
nodes.forEach((node,i)=>{
let offset=i-selected;
if(offset>total/2) offset-=total;
if(offset<-total/2) offset+=total;
const angle=(offset/total)*Math.PI*2;
const x=Math.sin(angle)*rX;
const y=(Math.cos(angle)*rY) - perspectiveTilt;
let abs=Math.abs(offset);
let size;
if(abs===0) size=110;
else if(abs===1) size=80;
else if(abs===2) size=55;
else size=30;
node.style.width=size+"px";
node.style.height=size+"px";
node.style.transform=
`translate(-50%,-50%) translate(${x}px,${y}px)`;
node.style.zIndex=100-abs;
node.style.opacity = (abs>3) ? 0.25 : 1;
node.classList.toggle("front",abs===0);
});
const title=document.querySelector(".title");
if(title) title.innerText = activeMenu[selected].title;
}
/* ========================= */
/* BEZIER SPIRAL TRANSITION */
/* ========================= */
function spiralTransition(callback){
const nodes=document.querySelectorAll(".game");
const rect=carousel.getBoundingClientRect();
const centerX=rect.width/2;
const centerY=rect.height/2;
nodes.forEach(node=>{
const randAngle=Math.random()*Math.PI*2;
const randDist=200+Math.random()*300;
const targetX=centerX + Math.cos(randAngle)*randDist;
const targetY=centerY + Math.sin(randAngle)*randDist;
$(node).animate({
left:targetX,
top:targetY,
opacity:0
},{
duration:TRANSITION_DURATION/2,
easing:"swing",
complete:function(){
if(callback) callback();
}
});
});
}
/* ========================= */
/* MENU SWITCHING */
/* ========================= */
function enterSubMenu(){
if(activeMenu[selected].title === "Options"){
spiralTransition(()=>{
activeMenu = optionsMenu;
selected = 0;
inSubMenu = true;
createNodes();
updatePositions();
});
}
}
function returnToMain(){
spiralTransition(()=>{
activeMenu = mainMenu;
selected = 0;
inSubMenu = false;
createNodes();
updatePositions();
});
}
/* ========================= */
/* NAVIGATION */
/* ========================= */
function moveLeft(){
selected = (selected - 1 + activeMenu.length) % activeMenu.length;
updatePositions();
}
function moveRight(){
selected = (selected + 1) % activeMenu.length;
updatePositions();
}
document.addEventListener("keydown", e => {
switch(e.key){
case "a":
case "A":
case "ArrowLeft":
e.preventDefault();
moveLeft();
break;
case "d":
case "D":
case "ArrowRight":
e.preventDefault();
moveRight();
break;
case "Enter":
e.preventDefault();
if(inSubMenu && activeMenu[selected].title === "Back"){
returnToMain();
} else {
enterSubMenu();
}
break;
case "Escape":
case "Backspace":
if(inSubMenu){
returnToMain();
}
break;
}
});
/* ========================= */
/* INIT */
/* ========================= */
createNodes();
updatePositions();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment