Skip to content

Instantly share code, notes, and snippets.

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

  • Save EncodeTheCode/9233771f62c3169474c0f63eb6f4e9c9 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/9233771f62c3169474c0f63eb6f4e9c9 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 — Fixed Options Enter</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;}
/* Each game is square w/ 0.35em rounded corners */
.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;
will-change: transform;
}
.game img{
width:100%; height:100%; object-fit:cover; display:block; border-radius:inherit; pointer-events:none; backface-visibility:hidden;
}
/* SELECTED / FRONT styling: exactly 1px solid white border, subtle glow */
.game.front{
border:1px solid #fff; /* requested white 1px border */
box-shadow:
0 14px 36px rgba(0,0,0,0.6),
0 0 18px rgba(255,255,255,0.06);
}
.game.single{ z-index:9999!important; }
.game.single img{ width:var(--selected-size); height:var(--selected-size); border-radius:0.35em; box-shadow:0 28px 68px rgba(0,0,0,0.75); }
.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;}
.hud{position:absolute;bottom:26px;width:100%;text-align:center;color:#ddd;font-size:14px;pointer-events:none;}
.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;}
@media (max-width:900px){ .carousel{width:94vw;height:46vh;} .game-title{font-size:14px;bottom:70px;} }
.game:focus{outline:none;}
</style>
</head>
<body>
<div class="stage">
<div class="carousel" id="carousel" aria-hidden="false"></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" aria-label="Move left">A / ←</button>
<button class="arrowBtn" id="btnRight" aria-label="Move right">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>
/* ===========================
Configurable visual vars
=========================== */
let spacing = 0.75;
let perspectiveTilt = 0.75;
let baseOffset = 40;
let spiralStrength = 1.15;
let boomerangIntensity = 0.9;
let stage1Duration = 520;
let stage2Duration = 820;
/* ===========================
Menu data
=========================== */
const mainGames = [
{ title: 'Crash Bandicoot', color: '#b2362f' },
{ title: 'Spyro the Dragon', color: '#2f6fb2' },
{ title: 'Tekken 3', color: '#2fb280' },
{ title: 'Final Fantasy VII', color: '#b28b2f' },
{ title: 'Metal Gear Solid', color: '#8a2fb2' },
{ title: 'Ridge Racer', color: '#b22f6f' },
{ title: 'Tombi!', color: '#2fb29e' },
{ title: 'Barbie: Race &amp; Ride', color: '#6fb22f' },
{ title: 'Wipeout 2097', color: '#b24c2f' },
{ title: 'Worms Armageddon', color: '#2fb2b2' },
{ title: 'Tomb Raider 3', color: '#b24ca8' },
{ 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;')
.replace(/'/g,'&#39;');
}
function makeSVGData(title,color){
const safe = escapeForSVG(title);
const svg = `<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-family='Verdana, Arial, sans-serif' font-size='34' fill='#fff' dominant-baseline='middle' text-anchor='middle'>${safe}</text></svg>`;
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
}
function cubicBezierPoint(p0,p1,p2,p3,t){
const u = 1-t;
const x = u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x;
const y = u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y;
return {x,y};
}
function easeOutCubic(x){ return 1 - Math.pow(1-x,3); }
function signedAngle(raw){ while(raw<=-Math.PI) raw+=2*Math.PI; while(raw>Math.PI) raw-=2*Math.PI; return raw; }
/* ===========================
State & DOM
=========================== */
const $carousel = $('#carousel');
let menuData = mainGames.slice();
let nodes = [];
let N = 0;
let selected = 0;
let currentMenu = 'main';
let menuStack = [];
let animating = false;
let inSingleView = false;
let savedSelected = null;
/* ---------- Layout math ---------- */
function getLayoutParams(){
const rect = $carousel[0].getBoundingClientRect();
const baseRX = Math.min(rect.width,900) * 0.42;
const baseRY = Math.min(rect.height,500) * 0.34;
const rX = baseRX * spacing;
const rY = baseRY * spacing;
return { rX, rY, width:rect.width, height:rect.height };
}
function angleStepFor(n){ return (2*Math.PI/n); }
function sizeFromD(d){ if(d<=0.5){ const t=d/0.5; return 110 - (110-80)*t; } else { const t=(d-0.5)/0.5; return 80 - (80-55)*t; } }
/* ---------- Build menu DOM (create nodes once per 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 = makeSVGData(g.title.replace(/&amp;/g,'&'), g.color || '#444');
el.appendChild(img);
$carousel.append(el);
nodes.push(el);
});
N = nodes.length;
nodes.forEach((node,idx)=>{
node.addEventListener('click', ()=>{
if(inSingleView){
restoreFromSingleView().then(()=>{ selected = idx; updatePositions(); updateGameTitle(); });
return;
}
selected = idx; updatePositions(); updateGameTitle();
});
node.addEventListener('keydown', (ev)=>{
if(ev.key === 'Enter' || ev.key === ' '){
ev.preventDefault();
if(!animating && !inSingleView){
selected = Number(node.dataset.index);
handleEnterOnSelected();
}
}
});
});
}
/* ---------- Circle layout for index i ---------- */
function circlePosFor(i, sel, count, params){
const step = angleStepFor(count);
const raw = signedAngle((i - sel) * 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 * perspectiveTilt + baseOffset;
const scale = size / 110;
return {x,y,scale,raw,d,size};
}
/* ---------- Update positions with CSS transitions for natural nav ----------
Also ensures exactly one `.front` item, focuses it, and sets aria-current.
*/
function updatePositions(){
if(inSingleView) return;
if(!nodes.length) return;
const params = getLayoutParams();
const step = angleStepFor(N);
let frontIndex = null;
for(let i=0;i<N;i++){
const node = nodes[i];
const pos = circlePosFor(i, selected, N, params);
node.style.setProperty('--size', pos.size + 'px');
node.style.width = pos.size + 'px';
node.style.height = pos.size + 'px';
node.style.transform = `translate(-50%,-50%) translate(${pos.x}px, ${pos.y}px) scale(${pos.scale}) rotate(0deg)`;
node.style.zIndex = Math.round((1 - pos.d) * 1000);
node.style.opacity = 1;
// decide front exactly using angle threshold
if(Math.abs(pos.raw) < (step * 0.5)){
node.classList.add('front');
node.setAttribute('aria-current','true');
frontIndex = i;
} else {
node.classList.remove('front');
node.removeAttribute('aria-current');
}
}
// If no one fell within threshold (edge cases), explicitly mark selected as front
if(frontIndex === null && nodes[selected]){
nodes[selected].classList.add('front');
nodes[selected].setAttribute('aria-current','true');
frontIndex = selected;
}
// Focus the frontIndex element so keyboard nav always originates from the selected item
try {
if(frontIndex !== null && nodes[frontIndex]) nodes[frontIndex].focus();
} catch(e){ /* ignore focus errors */ }
updateGameTitle();
}
/* ---------- Entrance animation ---------- */
function animateEntrance(durationOverride){
if(!nodes.length) return;
const params = getLayoutParams();
const targets = [];
for(let i=0;i<N;i++){
const pos = circlePosFor(i, selected, N, params);
targets.push(pos);
const node = nodes[i];
node.style.transform = `translate(-50%,-50%) translate(0px, 0px) scale(0.12) rotate(0deg)`;
node.style.opacity = 0;
node.classList.remove('single');
}
nodes.forEach(n => $(n).stop(true,true));
animating = true;
const dur = durationOverride || 900;
$({t:0}).animate({t:1},{duration:dur,easing:'swing',step(now){
const p = now; const e = easeOutCubic;
for(let i=0;i<N;i++){
const node = nodes[i], tar = targets[i];
const spin = (1 - p) * (3 * 2 * Math.PI);
const angle = tar.raw + spin;
const x = Math.sin(angle) * tar.x * e(p);
const y = Math.cos(angle) * tar.y * e(p);
const scale = 0.12 + (tar.scale - 0.12) * e(p);
node.style.transform = `translate(-50%,-50%) translate(${x}px, ${y}px) scale(${scale}) rotate(0deg)`;
node.style.opacity = Math.min(1, p*1.05);
node.style.zIndex = tar.size;
}
},complete(){
animating = false; updatePositions(); try{ nodes[selected].focus(); }catch(e){}
}});
}
/* ===========================
Bézier boomerang transition
=========================== */
/* Capture start circle states */
function captureStartStates(){
const params = getLayoutParams();
const startStates = [];
for(let i=0;i<N;i++){
const pos = circlePosFor(i, selected, N, params);
startStates.push({ x:pos.x, y:pos.y, scale:pos.scale, raw:pos.raw, size:pos.size });
}
return startStates;
}
/* Generate peaks using spiral math + randomness */
function generatePeaksFor(startStates){
const params = getLayoutParams();
return startStates.map((s, idx) => {
const spiralJitter = (Math.random()-0.5) * Math.PI * 0.6;
const angle = s.raw + spiralJitter;
const radius = Math.sqrt(params.rX*params.rX + params.rY*params.rY) * (spiralStrength + Math.random()*0.6);
const spiralRotation = (idx / Math.max(1,N)) * Math.PI * (0.2 + Math.random()*1.6);
const px = Math.sin(angle + spiralRotation) * radius;
const py = Math.cos(angle + spiralRotation) * params.rY * (perspectiveTilt * (1.0 + Math.random()*0.4)) - baseOffset * (0.2 + Math.random()*0.6);
const rot = (Math.random()<0.5? -1:1) * (220 + Math.random()*720);
const scale = Math.max(0.28, s.scale * (0.7 + Math.random()*0.6));
const ctrlOffset = Math.min(params.rX, params.rY) * (0.25 + Math.random()*0.8);
const p1 = { x: s.x + Math.cos(s.raw + Math.PI/2)*ctrlOffset, y: s.y + Math.sin(s.raw + Math.PI/2)*ctrlOffset };
const p2 = { x: px + Math.cos(s.raw - Math.PI/2)*ctrlOffset*0.6, y: py + Math.sin(s.raw - Math.PI/2)*ctrlOffset*0.6 };
return { px, py, rot, scale, p1, p2 };
});
}
/* Compute final positions for a menu (without DOM) */
function computeFinalPositionsForMenu(menuArray, selIndex){
const params = getLayoutParams();
const final = [];
const count = menuArray.length;
const step = angleStepFor(count);
for(let i=0;i<count;i++){
const raw = signedAngle((i - selIndex) * 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 * perspectiveTilt + baseOffset;
const scale = size / 110;
final.push({x,y,scale,raw,d,size});
}
return final;
}
/* Main transition routine */
function transitionToMenu(newMenuArray, title, newSelectedIndex = 0){
if(animating) return;
animating = true;
const startStates = captureStartStates();
const peaks = generatePeaksFor(startStates);
// Stage 1: start -> peak (cubic bezier outward)
const stage1Dur = Math.round(stage1Duration * (0.9 + Math.random()*0.25));
const stage1Dfd = $.Deferred();
$({t:0}).animate({t:1},{duration:stage1Dur,easing:'swing',step(now){
const p = now; const e = easeOutCubic;
for(let i=0;i<N;i++){
const node = nodes[i];
const s = startStates[i];
const pk = peaks[i];
const P0 = {x:s.x, y:s.y};
const P1 = pk.p1;
const P2 = pk.p2;
const P3 = {x:pk.px, y:pk.py};
const pt = cubicBezierPoint(P0,P1,P2,P3, e(p));
const rot = pk.rot * (p);
const sc = s.scale + (pk.scale - s.scale) * e(p);
node.style.transform = `translate(-50%,-50%) translate(${pt.x}px, ${pt.y}px) scale(${sc}) rotate(${rot}deg)`;
node.style.opacity = 1 - Math.min(0.6, p*0.9);
node.style.zIndex = Math.round(10000 - Math.abs(pt.x) - Math.abs(pt.y));
}
}, complete(){
stage1Dfd.resolve();
}});
// After outward stage, swap DOM mid-air and animate back in
stage1Dfd.promise().then(()=>{
const outwardPositions = peaks.map(pk => ({x:pk.px, y:pk.py, rot:pk.rot, scale:pk.scale, opacity: 0.2 + Math.random()*0.6}));
// Save state so return works
menuStack.push({ menu: currentMenu, data: menuData.slice(), selectedIndex: selected, title: $('#menuTitle').text() });
// don't set currentMenu here — set it after transition finishes
// Build new DOM — start nodes at outward positions
menuData = newMenuArray.slice();
$carousel.empty(); nodes = [];
menuData.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 = makeSVGData(g.title.replace(/&amp;/g,'&'), g.color || '#444');
el.appendChild(img);
$carousel.append(el);
nodes.push(el);
});
N = nodes.length;
nodes.forEach((node,idx)=>{
node.addEventListener('click', ()=>{
if(inSingleView){
restoreFromSingleView().then(()=>{ selected = idx; updatePositions(); updateGameTitle(); });
return;
}
selected = idx; updatePositions(); updateGameTitle();
});
node.addEventListener('keydown', (ev)=>{
if(ev.key === 'Enter' || ev.key === ' '){
ev.preventDefault();
if(!animating && !inSingleView){
selected = Number(node.dataset.index);
handleEnterOnSelected();
}
}
});
});
// position new nodes at outward peaks (cycle if counts mismatch)
for(let i=0;i<N;i++){
const pos = outwardPositions[i % outwardPositions.length];
const node = nodes[i];
node.style.transform = `translate(-50%,-50%) translate(${pos.x}px, ${pos.y}px) scale(${pos.scale}) rotate(${pos.rot}deg)`;
node.style.opacity = pos.opacity;
node.style.width = '60px';
node.style.height = '60px';
}
// compute final positions and animate stage2 (peak -> final) along bezier boomerang arcs
const finalPositions = computeFinalPositionsForMenu(menuData, newSelectedIndex);
const stage2Dur = Math.round(stage2Duration * (0.9 + Math.random()*0.2));
$({t:0}).animate({t:1},{duration:stage2Dur,easing:'swing',step(now){
const p = now; const e = easeOutCubic;
for(let i=0;i<N;i++){
const node = nodes[i];
const peak = outwardPositions[i % outwardPositions.length];
const fin = finalPositions[i];
const P0 = {x: peak.x, y: peak.y};
const angle = Math.atan2(fin.y - peak.y, fin.x - peak.x);
const cpMag = Math.min(200, Math.hypot(peak.x-fin.x, peak.y-fin.y) * (0.4 + Math.random()*0.6));
const cp1 = { x: peak.x + Math.cos(angle + Math.PI/2) * cpMag * (0.6 + Math.random()*0.8), y: peak.y + Math.sin(angle + Math.PI/2) * cpMag * (0.6 + Math.random()*0.8) };
const cp2 = { x: fin.x + Math.cos(angle - Math.PI/2) * cpMag * (0.3 + Math.random()*0.8), y: fin.y + Math.sin(angle - Math.PI/2) * cpMag * (0.3 + Math.random()*0.8) };
const t = e(p);
const pt = cubicBezierPoint(P0, cp1, cp2, {x:fin.x, y:fin.y}, t);
const rot = (peak.rot || 0) * (1 - p * (1.0 + boomerangIntensity*0.2));
const scale = peak.scale + (fin.scale - peak.scale) * t;
node.style.transform = `translate(-50%,-50%) translate(${pt.x}px, ${pt.y}px) scale(${scale}) rotate(${rot}deg)`;
node.style.opacity = Math.min(1, 0.2 + p*1.4);
node.style.zIndex = Math.round((1 - fin.d) * 1000);
node.style.setProperty('--size', fin.size + 'px');
node.style.width = fin.size + 'px';
node.style.height = fin.size + 'px';
}
}, complete(){
animating = false;
$('#menuTitle').text(title || 'Menu');
selected = newSelectedIndex;
// set currentMenu consistently depending on the menu we transitioned to
currentMenu = (newMenuArray === optionsGames) ? 'options' : 'main';
updatePositions();
try{ nodes[selected].focus(); }catch(e){}
}});
});
}
/* ---------- Helpers to open Options & return ---------- */
function openOptions(){
// If already animating, ignore
if(animating) return;
transitionToMenu(optionsGames, 'Options', 0);
}
function returnToParent(){
const prev = menuStack.pop();
if(!prev){ animating = false; return; }
transitionToMenu(prev.data.slice(), prev.title || 'Main Menu', prev.selectedIndex || 0);
// currentMenu will be updated at end of transitionToMenu
}
/* ---------- Title update ---------- */
function updateGameTitle(){
const el = document.getElementById('gameTitle');
if(!el) return;
const item = (menuData && menuData[selected]) ? menuData[selected].title : '';
el.textContent = String(item).replace(/&amp;/g,'&');
}
/* ---------- Enter / select handling (robust) ---------- */
function handleEnterOnSelected(){
const item = menuData[selected];
if(!item) return;
const realTitle = String(item.title).replace(/&amp;/g,'&').trim().toLowerCase();
// Open Options whenever the selected item's title is exactly "options" (resilient)
if(realTitle === 'options'){
openOptions();
return;
}
// Back handling inside options
if(realTitle === 'back'){
returnToParent();
return;
}
enterSelect();
}
/* ---------- Single item view ---------- */
function enterSelect(){
if(animating || inSingleView) return $.Deferred().resolve().promise();
animating = true; inSingleView = true; savedSelected = selected;
nodes.forEach(n=>$(n).stop(true,true));
const chosen = nodes[selected]; const img = chosen.querySelector('img');
const curSize = parseFloat(getComputedStyle(chosen).getPropertyValue('--size')) || 100;
nodes.forEach((node,idx)=>{ if(idx===selected) return; $(node).animate({opacity:0},260).promise().done(()=>{ node.style.pointerEvents='none'; }); });
const dfd = $.Deferred();
$({t:0}).animate({t:1},{duration:420,easing:'swing',step(now){
const p=now; const curScale=(curSize/110); const targetScale=(parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--selected-size'))||200)/110;
const scale = curScale + (targetScale - curScale) * p;
chosen.style.transform = `translate(-50%,-50%) translate(0px,0px) scale(${scale})`;
chosen.style.zIndex = 9999; chosen.style.opacity = 1;
if(img){ const sizePx = (curSize*(1-p)) + ((parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--selected-size'))||200)*p); img.style.width = sizePx + 'px'; img.style.height = sizePx + 'px'; }
},complete(){ chosen.classList.add('single'); animating=false; dfd.resolve(); }});
return dfd.promise();
}
function restoreFromSingleView(){
if(!inSingleView && !animating) return $.Deferred().resolve().promise();
animating = true;
nodes.forEach(n=>$(n).stop(true,true));
const promises = nodes.map(node => {
const d = $.Deferred();
node.style.pointerEvents = 'auto';
$(node).animate({opacity:1},240, function(){
const img = node.querySelector('img');
if(img){ img.style.width=''; img.style.height=''; }
d.resolve();
});
return d.promise();
});
return $.when(...promises).then(()=>{ nodes.forEach(n=>{ n.classList.remove('single'); n.style.zIndex=''; }); inSingleView=false; animating=false; if(savedSelected!==null){ selected = savedSelected; savedSelected = null; } setTimeout(updatePositions,20); });
}
/* ---------- Navigation ---------- */
function moveLeft(){ if(inSingleView){ restoreFromSingleView().then(()=>{ selected = (selected - 1 + N) % N; updatePositions(); }); return; } selected = (selected - 1 + N) % N; updatePositions(); }
function moveRight(){ if(inSingleView){ restoreFromSingleView().then(()=>{ selected = (selected + 1) % N; updatePositions(); }); return; } selected = (selected + 1) % N; updatePositions(); }
/* ---------- Keyboard & UI bindings ---------- */
$(window).on('keydown', (ev)=>{
const tag = (document.activeElement && document.activeElement.tagName) || "";
if(tag === "INPUT" || tag === "TEXTAREA") return;
const k = ev.key;
if(k === 'a' || k === 'A' || k === 'ArrowLeft'){ ev.preventDefault(); moveLeft(); }
else if(k === 'd' || k === 'D' || k === 'ArrowRight'){ ev.preventDefault(); moveRight(); }
else if(k === 'Enter'){ ev.preventDefault(); if(!animating && !inSingleView) handleEnterOnSelected(); }
else if(k === 'r' || k === 'R'){ ev.preventDefault(); if(inSingleView && !animating) restoreFromSingleView(); else if(currentMenu==='options' && !animating) returnToParent(); }
});
$('#btnLeft').on('click', moveLeft); $('#btnRight').on('click', moveRight);
$(window).on('resize', ()=>{ setTimeout(updatePositions,60); });
/* ---------- Init ---------- */
function initMain(){
menuData = mainGames.slice();
buildMenu(menuData);
selected = 0;
$('#menuTitle').text('Main Menu');
setTimeout(()=>{ animateEntrance(19000); }, 50);
}
$(function(){ initMain(); });
/* ---------- Runtime API ---------- */
window._ps = {
setSpacing: v=>{ spacing = Number(v)||1.0; updatePositions(); },
setTilt: v=>{ perspectiveTilt = Number(v)||0.75; updatePositions(); },
setBaseOffset: v=>{ baseOffset = Number(v)||40; updatePositions(); },
setSpiralStrength: v=>{ spiralStrength = Number(v)||1.15; },
setBoomerangIntensity: v=>{ boomerangIntensity = Number(v)||0.9; },
setStageDurations: (s1,s2)=>{ stage1Duration = s1||520; stage2Duration = s2||820; },
openOptions: ()=>openOptions(),
returnToParent: ()=>returnToParent(),
enterSelect: ()=>enterSelect(),
restoreFromSingleView: ()=>restoreFromSingleView()
};
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment