Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created March 10, 2026 01:11
Show Gist options
  • Select an option

  • Save EncodeTheCode/9b88e87f11e9e58be1baa61cc8fa6e3a to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/9b88e87f11e9e58be1baa61cc8fa6e3a 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>PS1 Demo One — Underwater Menu (HTML5 + jQuery)</title>
<!-- jQuery (CDN) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
/* --- Basic page / layout --- */
html,body{
height:100%;
margin:0;
background: #700; /* tiny border like your screenshot */
font-family: "Trebuchet MS", Arial, sans-serif;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
overflow:hidden;
}
/* Stage */
.stage{
width:100%;
height:100%;
display:flex;
align-items:center;
justify-content:center;
position:relative;
box-sizing:border-box;
padding:30px;
}
/* skybox / underwater background */
.skybox{
position:absolute;
inset:30px;
border-radius:2px;
overflow:hidden;
z-index:0;
background:
radial-gradient(ellipse at 20% 30%, rgba(0,180,120,0.15), transparent 10%),
radial-gradient(ellipse at 80% 70%, rgba(0,100,255,0.12), transparent 15%),
linear-gradient(180deg,#083b5a 0%, #0b4c52 35%, #1b5f58 60%, #0b2b4a 100%);
box-shadow: inset 0 0 80px rgba(0,0,0,0.6);
transform-origin:center center;
will-change:transform, filter;
}
/* optional external PNG (your demo1 screenshot) */
.skybox::before{
content:"";
position:absolute;
inset:-20%;
background-image: url('demo1.png');
background-size:cover;
background-position:center;
opacity:0.08; /* subtle, to keep demo-like look */
mix-blend-mode:screen;
filter: contrast(1.1) saturate(1.05);
transition:opacity .6s;
}
/* gentle "sway" plus very slow lap rotation to create non-repeating movement */
@keyframes sky-sway {
0% { transform: rotate(-6deg) translateZ(0); }
40% { transform: rotate( 6deg) translateZ(0); }
100% { transform: rotate(360deg) translateZ(0); }
}
.skybox {
animation:
sky-sway 40s cubic-bezier(.25,.1,.25,1) infinite;
transform-origin:center center;
}
/* small light ripples overlay */
.skybox::after{
content:"";
position:absolute;
inset:0;
background:
radial-gradient(circle at 30% 20%, rgba(255,255,255,0.03), transparent 10%),
radial-gradient(circle at 70% 80%, rgba(180,255,200,0.02), transparent 15%);
mix-blend-mode:overlay;
pointer-events:none;
animation: floaty 12s linear infinite;
}
@keyframes floaty { from {transform:translateY(0)} to {transform:translateY(-18px)} }
/* UI area on top */
.ui {
position:relative;
z-index:5;
width:80%;
max-width:1200px;
height:80%;
display:flex;
align-items:center;
justify-content:center;
perspective:1100px;
}
/* center container (3D ring) */
.menu-wrapper{
width:830px;
height:420px;
position:relative;
display:flex;
align-items:center;
justify-content:center;
transform-style:preserve-3d;
}
/* the ring that will rotate in Y */
.menu3d{
position:absolute;
width:100%;
height:100%;
transform-style:preserve-3d;
transition: transform 800ms cubic-bezier(.23,.9,.31,1);
pointer-events:none;
}
/* each item (label + dot) */
.menu-item{
position:absolute;
left:50%;
top:48%;
transform-origin:center center;
backface-visibility:hidden;
text-align:center;
pointer-events:none;
will-change:transform,opacity,filter;
}
/* Stylized label text — bright lime gradient + glow + rounded blur border */
.label {
display:inline-block;
font-size:54px;
letter-spacing:1px;
font-weight:800;
padding:6px 14px;
border-radius:14px;
line-height:1;
text-transform:capitalize;
position:relative;
z-index:3;
color:transparent;
background: linear-gradient(180deg, #c7ff45 0%, #8ecc23 100%);
-webkit-background-clip: text;
background-clip: text;
text-shadow:
0 0 18px rgba(180,255,120,0.18),
0 0 8px rgba(0,0,0,0.5);
filter: drop-shadow(0 6px 12px rgba(0,0,0,0.55));
transform-origin:center center;
}
/* border / glow around label (outer) */
.label::before{
content:"";
position:absolute;
left:-8px; right:-8px; top:-6px; bottom:-6px;
border-radius:16px;
z-index:-1;
background:linear-gradient(90deg, rgba(0,0,0,0.18), rgba(255,255,255,0.02));
filter: blur(6px) saturate(1.1);
opacity:0.6;
}
/* Big DOT separator */
.dot {
display:inline-block;
width:26px;
height:26px;
margin-left:12px;
border-radius:50%;
background: radial-gradient(circle at 35% 30%, #ffd55c, #ff8b39);
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
vertical-align:middle;
transform:translateY(-6px);
}
/* active appears front and bigger */
.menu-item.active .label{
transform: translateZ(30px) scale(1.18);
text-shadow:
0 0 28px rgba(200,255,120,0.35), 0 2px 10px rgba(0,0,0,0.7);
filter: none;
}
.menu-item.inactive .label{
opacity:0.35;
transform: translateZ(-60px) scale(.88);
filter: grayscale(.2) blur(0.4px) brightness(.8);
}
/* items behind star must appear partially behind — star uses z-index layering */
.menu-item.behind{
opacity:0.42;
transform-origin:center center;
}
/* center starfish (SVG) */
.starfish-wrap{
position:absolute;
z-index:2;
left:50%;
top:50%;
transform:translate(-50%,-50%);
width:420px;
height:420px;
pointer-events:none;
display:flex;
align-items:center;
justify-content:center;
transform-style:preserve-3d;
}
.starfish{
width:100%;
height:100%;
transform-origin:center center;
animation: star-spin 28s linear infinite;
filter: drop-shadow(0 20px 24px rgba(0,0,0,0.6));
opacity:0.98;
}
@keyframes star-spin{
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* detail area (selected item or inner game name) */
.detail {
position:absolute;
bottom:18%;
left:50%;
transform:translateX(-50%);
z-index:6;
font-weight:700;
color:#dfffa8;
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
font-size:24px;
display:flex;
gap:10px;
align-items:center;
user-select:none;
}
.detail .title{
background:linear-gradient(180deg, rgba(255,255,255,0.04), rgba(0,0,0,0.04));
padding:8px 14px;
border-radius:10px;
border:1px solid rgba(255,255,255,0.03);
box-shadow: inset 0 2px 10px rgba(0,0,0,0.2);
}
/* instructions */
.help {
position:absolute;
z-index:8;
left:22px; bottom:22px;
color:rgba(255,255,255,0.76);
font-size:12px;
background:rgba(0,0,0,0.25);
padding:8px 12px;
border-radius:6px;
border:1px solid rgba(255,255,255,0.02);
}
/* responsive */
@media (max-width:900px){
.menu-wrapper{ width:90%; height:360px;}
.label{ font-size:36px;}
.starfish-wrap{ width:300px; height:300px;}
}
</style>
</head>
<body>
<div class="stage">
<div class="skybox" id="skybox"></div>
<div class="ui">
<div class="menu-wrapper" aria-hidden="false">
<!-- rotating ring -->
<div class="menu3d" id="menu3d">
<!-- items inserted by JS -->
</div>
<!-- starfish in middle -->
<div class="starfish-wrap" aria-hidden="true">
<!-- Hand-made SVG starfish with a bit of noisy texture -->
<svg class="starfish" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
<defs>
<radialGradient id="sfGrad" cx="30%" cy="30%">
<stop offset="0%" stop-color="#ff7a2c"/>
<stop offset="50%" stop-color="#e33b2b"/>
<stop offset="100%" stop-color="#b71a1a"/>
</radialGradient>
<filter id="grain">
<feTurbulence baseFrequency="0.9" numOctaves="1" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
<feBlend mode="overlay"/>
</filter>
<filter id="soft" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.6" />
</filter>
</defs>
<g transform="translate(300,300)">
<!-- organic star-like shape -->
<path d="M0 -220 C 50 -160 110 -110 200 -90 C 120 -10 150 40 210 120 C 110 60 45 120 5 210 C -40 120 -120 60 -220 120 C -160 40 -120 -10 -200 -90 C -110 -110 -50 -160 0 -220 Z"
fill="url(#sfGrad)" stroke="#8b1515" stroke-width="8" filter="url(#soft)" />
<!-- spots -->
<g fill="#ffb07a" opacity="0.9">
<circle cx="-40" cy="-40" r="16"/>
<circle cx="90" cy="-40" r="12"/>
<circle cx="40" cy="40" r="10"/>
<circle cx="-80" cy="60" r="8"/>
<circle cx="130" cy="80" r="9"/>
</g>
<path d="M -20 -40 q 20 -24 40 0" stroke="#a00000" stroke-width="2" fill="none" opacity="0.7"/>
</g>
</svg>
</div>
<!-- detail / current selection text -->
<div class="detail" id="detail">
<div class="title" id="detailMain">Games</div>
<div class="title" id="detailSub">Tomb Raider 3</div>
</div>
<div class="help">
Left / Right — rotate menu. <br/>
When <strong>Games</strong> is centered, Left/Right cycle games. <br/>
Enter — toggle selecting (visual only), Esc — reset
</div>
</div>
</div>
</div>
<script>
$(function(){
// configuration
const mainLabels = ["games","video","music","memory","network"];
const gamesList = ["Tomb Raider 3","Tekken 3","Crash Bandicoot","Final Fantasy VII","Metal Gear Solid"];
const radius = 360; // translateZ radius for circle
const menu3d = $('#menu3d');
let currentIndex = 0; // which mainLabel is front
let currentGameIndex = 0;
let selectedMode = false; // Enter toggles 'selected' (visual)
const n = mainLabels.length;
const angleStep = 360 / n;
// create items
mainLabels.forEach((label, i) => {
const $it = $(`
<div class="menu-item" data-i="${i}">
<div class="label">${label}</div>
<div class="dot" aria-hidden="true"></div>
</div>
`);
menu3d.append($it);
});
// position items around circle (rotateY + translateZ)
function layoutItems(){
menu3d.find('.menu-item').each(function(){
const i = Number($(this).attr('data-i'));
// compute angle so that index 0 is at front (0deg)
const angle = i * angleStep;
const transform = `rotateY(${angle}deg) translateZ(${radius}px) translateX(-50%)`;
$(this).css('transform', transform);
});
updateVisual();
}
layoutItems();
// set the ring transform to show currentIndex at front
function updateVisual(){
const rotationY = -currentIndex * angleStep;
menu3d.css('transform', `translateZ(-${radius}px) rotateY(${rotationY}deg)`);
// update classes for items (active/inactive/behind)
menu3d.find('.menu-item').each(function(){
const i = Number($(this).attr('data-i'));
$(this).removeClass('active inactive behind');
if(i === currentIndex){
$(this).addClass('active');
} else {
// compute shortest angular distance
let d = Math.abs(i - currentIndex);
d = Math.min(d, n - d);
if(d === 1){
$(this).addClass('inactive');
} else {
$(this).addClass('behind');
}
}
});
// update detail main label and sublabel depending on front
$('#detailMain').text(capitalize(mainLabels[currentIndex]));
if(mainLabels[currentIndex] === 'games'){
$('#detailSub').text(gamesList[currentGameIndex]);
} else {
$('#detailSub').text('');
}
}
// keyboard navigation
$(window).on('keydown', function(e){
const LEFT = 37, RIGHT = 39, ENTER = 13, ESC = 27, UP=38, DOWN=40;
if(e.keyCode === LEFT || e.keyCode === RIGHT){
e.preventDefault();
const dir = (e.keyCode === RIGHT) ? 1 : -1;
// If current main label is 'games', left/right cycles gamesList
if(mainLabels[currentIndex] === 'games'){
// cycle inner games
currentGameIndex = (currentGameIndex + dir + gamesList.length) % gamesList.length;
// visually animate sub text
$('#detailSub').stop(true).animate({ opacity:0 }, 140, function(){
$(this).text(gamesList[currentGameIndex]).css('opacity',0).animate({opacity:1},300);
});
// also add subtle star bounce for feedback
$('.starfish').stop(true).animate({ rotation: "+=6" }, { duration:260, step:function(now){$(this).css('transform','rotate('+now+'deg)')}});
} else {
// rotate main menu
currentIndex = (currentIndex + dir + n) % n;
// reset game index when leaving games
currentGameIndex = 0;
updateVisual();
}
} else if(e.keyCode === ENTER){
// toggle selection visual
selectedMode = !selectedMode;
if(selectedMode){
$('.menu-item.active .label').css('text-shadow','0 0 36px rgba(255,255,120,0.5), 0 3px 14px rgba(0,0,0,0.6)');
$('#detailMain').css('transform','scale(1.05)');
} else {
$('.menu-item.active .label').css('text-shadow','0 0 28px rgba(200,255,120,0.35), 0 2px 10px rgba(0,0,0,0.7)');
$('#detailMain').css('transform','scale(1)');
}
} else if(e.keyCode === ESC){
// reset to index 0
currentIndex = 0; currentGameIndex = 0; selectedMode = false;
updateVisual();
} else if(e.keyCode === UP || e.keyCode === DOWN){
// alternative rotation using up/down for convenience
const dir = (e.keyCode === DOWN) ? 1 : -1;
currentIndex = (currentIndex + dir + n) % n;
updateVisual();
}
});
// Utility: capitalize
function capitalize(s){ return s.charAt(0).toUpperCase() + s.slice(1); }
// initial visual
updateVisual();
// responsiveness: recalc radius on resize (so ring still looks right)
$(window).on('resize', function(){
// simple approach: adjust radius proportional to width
const w = $('.menu-wrapper').width();
// clamp
const newRadius = Math.max(220, Math.min(520, Math.round(w * 0.45)));
// set radius by updating styles (recompute transforms)
// NOTE: using same variable isn't straightforward; we manually update each item
menu3d.find('.menu-item').each(function(){
const i = Number($(this).attr('data-i'));
const angle = i * angleStep;
const transform = `rotateY(${angle}deg) translateZ(${newRadius}px) translateX(-50%)`;
$(this).css('transform', transform);
});
// and the ring translateZ
menu3d.css('transform', function(_, old){
// compute using currentIndex
const rotationY = -currentIndex * angleStep;
return `translateZ(-${newRadius}px) rotateY(${rotationY}deg)`;
});
}).trigger('resize');
// small star wobble occasionally to feel organic
setInterval(()=> {
$('.starfish').animate({opacity:0.96}, 600)
.animate({opacity:1}, 1600);
}, 4500);
// mouse/touch: clicking left/right areas to rotate (for convenience)
// create large invisible left/right click zones
const $leftZone = $('<div>').css({
position:'absolute', left:0, top:0, bottom:0, width:'35%',
zIndex:9, cursor:'w-resize', opacity:0, pointerEvents:'auto'
}).appendTo('body').on('click', ()=> $(window).trigger($.Event('keydown', { keyCode:37 })));
const $rightZone = $leftZone.clone().css({left:'65%', right:0}).appendTo('body').on('click', ()=> $(window).trigger($.Event('keydown', { keyCode:39 })));
// friendly focus to capture keys
$(window).focus();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment