Created
March 10, 2026 02:00
-
-
Save EncodeTheCode/96b75e7de51ef395c696b48121f03dfb 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>PS1 Demo-One — Curved 3D Ribbon Menu</title> | |
| <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> | |
| <style> | |
| :root{ | |
| --menu-radius:380px; | |
| --label-size:48px; | |
| --dot-size:32px; | |
| --ribbon-thickness:72px; | |
| --front-boost:48px; | |
| } | |
| html,body{ | |
| height:100%; | |
| margin:0; | |
| background:#00121a; | |
| font-family:"Trebuchet MS",Arial,sans-serif; | |
| overflow:hidden; | |
| } | |
| #bgCanvas{ | |
| position:fixed; | |
| inset:0; | |
| width:100%; | |
| height:100%; | |
| z-index:0; | |
| } | |
| .stage{ | |
| position:relative; | |
| width:100%; | |
| height:100%; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| perspective:1200px; | |
| z-index:2; | |
| } | |
| .ui{ | |
| width:92%; | |
| max-width:1280px; | |
| height:86%; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| position:relative; | |
| } | |
| .menu-wrapper{ | |
| width:920px; | |
| height:520px; | |
| position:relative; | |
| transform-style:preserve-3d; | |
| } | |
| .menu3d{ | |
| position:absolute; | |
| inset:0; | |
| transform-style:preserve-3d; | |
| z-index:50; | |
| } | |
| .menu-item{ | |
| position:absolute; | |
| left:50%; | |
| top:50%; | |
| transform-origin:center; | |
| transform-style:preserve-3d; | |
| transition:transform .4s cubic-bezier(.22,.95,.3,1); | |
| } | |
| svg.ribbon{ | |
| width:760px; | |
| height:260px; | |
| overflow:visible; | |
| display:block; | |
| } | |
| svg.ribbon text{ | |
| font-size:var(--label-size); | |
| font-weight:900; | |
| paint-order:stroke fill; | |
| stroke:black; | |
| stroke-width:2; | |
| } | |
| svg.ribbon text tspan{ | |
| fill:#f8ff8a; | |
| } | |
| .menu-item.front{opacity:1;z-index:120} | |
| .menu-item.side{opacity:.45} | |
| .menu-item.back{opacity:.2} | |
| .star-wrap{ | |
| position:absolute; | |
| left:50%; | |
| top:50%; | |
| transform:translate(-50%,-50%); | |
| width:420px; | |
| height:420px; | |
| z-index:40; | |
| } | |
| #starImg{ | |
| max-width:100%; | |
| } | |
| .detail{ | |
| position:absolute; | |
| left:50%; | |
| bottom:6%; | |
| transform:translateX(-50%); | |
| color:#eaffc8; | |
| font-size:20px; | |
| z-index:160; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="bgCanvas"></canvas> | |
| <div class="stage"> | |
| <div class="ui"> | |
| <div class="menu-wrapper"> | |
| <div id="menu3d" class="menu3d"></div> | |
| <div class="star-wrap"> | |
| <img id="starImg"/> | |
| </div> | |
| <div class="detail"> | |
| <div id="detailMain">Games</div> | |
| <div id="detailSub"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* background */ | |
| (function(){ | |
| const canvas=document.getElementById("bgCanvas") | |
| const ctx=canvas.getContext("2d") | |
| let W=canvas.width=innerWidth | |
| let H=canvas.height=innerHeight | |
| const blobs=[] | |
| for(let i=0;i<30;i++){ | |
| blobs.push({ | |
| x:Math.random()*W, | |
| y:Math.random()*H, | |
| r:120+Math.random()*180, | |
| vx:(Math.random()*2-1)*0.2, | |
| vy:(Math.random()*2-1)*0.2 | |
| }) | |
| } | |
| function draw(){ | |
| ctx.clearRect(0,0,W,H) | |
| ctx.globalCompositeOperation="lighter" | |
| ctx.filter="blur(25px)" | |
| for(const b of blobs){ | |
| ctx.fillStyle="rgba(40,180,200,0.25)" | |
| ctx.beginPath() | |
| ctx.ellipse(b.x,b.y,b.r,b.r*.8,0,0,Math.PI*2) | |
| ctx.fill() | |
| } | |
| ctx.filter="none" | |
| } | |
| function step(){ | |
| for(const b of blobs){ | |
| b.x+=b.vx | |
| b.y+=b.vy | |
| if(b.x<-b.r)b.x=W+b.r | |
| if(b.x>W+b.r)b.x=-b.r | |
| if(b.y<-b.r)b.y=H+b.r | |
| if(b.y>H+b.r)b.y=-b.r | |
| } | |
| draw() | |
| requestAnimationFrame(step) | |
| } | |
| step() | |
| })() | |
| /* menu */ | |
| $(function(){ | |
| const mainLabels=["games","video","music","memory","network"] | |
| const subMenus={ | |
| video:["Display Mode","Aspect Ratio","Color Settings"], | |
| memory:["Memory Card Slot 1","Memory Card Slot 2","Delete Save"] | |
| } | |
| const gamesList=[ | |
| "Tomb Raider 3", | |
| "Tekken 3", | |
| "Crash Bandicoot", | |
| "Final Fantasy VII", | |
| "Metal Gear Solid" | |
| ] | |
| const menu3d=$("#menu3d") | |
| const n=mainLabels.length | |
| const angleStep=360/n | |
| let currentRotation=0 | |
| let speedDeg=-6 | |
| let menuOpen=false | |
| let currentGameIndex=0 | |
| let radius=380 | |
| function createRibbonSVG(id,label){ | |
| const outR=radius*.9 | |
| const dotSpace=25 | |
| const dotX=outR+dotSpace | |
| return ` | |
| <svg class="ribbon" viewBox="-500 -200 1000 400"> | |
| <path d=" | |
| M -300 -40 | |
| Q 0 -80 | |
| 300 -40 | |
| L 300 40 | |
| Q 0 80 | |
| -300 40 | |
| Z | |
| " fill="#9ddc2f"/> | |
| <text x="0" y="10" text-anchor="middle"> | |
| <tspan>${label}</tspan> | |
| </text> | |
| <circle cx="${dotX}" cy="0" r="16" fill="#9ddc2f"/> | |
| </svg> | |
| ` | |
| } | |
| function buildItems(){ | |
| menu3d.empty() | |
| for(let i=0;i<n;i++){ | |
| const label=mainLabels[i] | |
| const item=$(`<div class="menu-item"></div>`) | |
| item.attr("data-i",i) | |
| item.html(createRibbonSVG(i,label)) | |
| menu3d.append(item) | |
| } | |
| } | |
| function frontIndexFromRotation(rot){ | |
| const normalized=(( -rot %360)+360)%360 | |
| return Math.round(normalized/angleStep)%n | |
| } | |
| function updateVisuals(){ | |
| menu3d.find(".menu-item").each(function(){ | |
| const i=Number($(this).attr("data-i")) | |
| const angle=i*angleStep+currentRotation | |
| const rad=angle*Math.PI/180 | |
| const curve=Math.sin(rad)*18 | |
| const depth=(Math.cos(rad)+1)/2 | |
| const scale=.8+depth*.4 | |
| const transform=` | |
| translate(-50%,-50%) | |
| rotateY(${angle}deg) | |
| translateZ(${radius}px) | |
| rotateY(${-angle}deg) | |
| rotateX(${curve}deg) | |
| scale(${scale}) | |
| ` | |
| $(this).css("transform",transform) | |
| }) | |
| const front=frontIndexFromRotation(currentRotation) | |
| menu3d.find(".menu-item").each(function(){ | |
| const i=$(this).data("i") | |
| $(this).removeClass("front side back") | |
| if(i===front)$(this).addClass("front") | |
| else if(Math.abs(i-front)==1)$(this).addClass("side") | |
| else $(this).addClass("back") | |
| }) | |
| const label=mainLabels[front] | |
| $("#detailMain").text(label) | |
| if(label==="games") | |
| $("#detailSub").text(gamesList[currentGameIndex]) | |
| else if(subMenus[label]) | |
| $("#detailSub").text(subMenus[label][0]) | |
| else | |
| $("#detailSub").text("") | |
| } | |
| function rafLoop(){ | |
| if(!menuOpen) | |
| currentRotation+=speedDeg*.016 | |
| updateVisuals() | |
| requestAnimationFrame(rafLoop) | |
| } | |
| $(window).on("keydown",function(e){ | |
| if(e.which==37){ | |
| currentRotation+=angleStep | |
| } | |
| if(e.which==39){ | |
| currentRotation-=angleStep | |
| } | |
| if(e.which==13){ | |
| menuOpen=true | |
| } | |
| if(e.which==27){ | |
| menuOpen=false | |
| } | |
| }) | |
| buildItems() | |
| rafLoop() | |
| }) | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment