Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created May 17, 2026 10:56
Show Gist options
  • Select an option

  • Save EncodeTheCode/476401b0f85cd4589f6ab38b2ad4d434 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/476401b0f85cd4589f6ab38b2ad4d434 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.0" />
<title>Demo One Orbit Menu</title>
<style>
:root {
--bg0: #020408;
--bg1: #05070b;
--text: #ecf3ff;
--muted: rgba(236, 243, 255, 0.62);
--perspective: 1400px;
--orbit-x: 340px;
--orbit-y: 54px;
--orbit-z: 180px;
--label-w: 200px;
--label-h: 56px;
--divider-w: 120px;
--divider-h: 32px;
--spin-speed: 16; /* degrees per second */
--stage-tilt: 18deg;
--glow: 0 0 24px rgba(137, 184, 255, 0.22);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at 50% 22%, rgba(85, 120, 180, 0.20), transparent 30%),
radial-gradient(circle at 50% 74%, rgba(120, 150, 235, 0.10), transparent 38%),
linear-gradient(180deg, var(--bg0) 0%, var(--bg1) 55%, #020406 100%);
color: var(--text);
font-family: "Trebuchet MS", "Verdana", "Segoe UI", sans-serif;
letter-spacing: 0.02em;
}
.app {
position: relative;
width: 100%;
height: 100%;
perspective: var(--perspective);
perspective-origin: 50% 50%;
overflow: hidden;
}
.scanlines {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to bottom, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 100% 4px;
opacity: 0.16;
mix-blend-mode: screen;
}
.vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(circle at center, transparent 0 54%, rgba(0,0,0,0.48) 100%);
}
.hud {
position: absolute;
left: 3vw;
top: 3vh;
z-index: 5;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
letter-spacing: 0.12em;
text-transform: uppercase;
user-select: none;
text-shadow: 0 0 10px rgba(0,0,0,0.65);
}
.scene {
position: absolute;
inset: 0;
display: grid;
place-items: center;
transform-style: preserve-3d;
}
.stage {
position: relative;
width: min(96vw, 1500px);
height: min(82vh, 900px);
transform-style: preserve-3d;
transform: translateY(8px) rotateX(var(--stage-tilt));
}
.orbit-anchor {
position: absolute;
left: 50%;
top: 50%;
width: 1px;
height: 1px;
transform-style: preserve-3d;
transform: translate(-50%, -50%);
}
.center-dot {
position: absolute;
left: 50%;
top: 50%;
width: 14px;
height: 14px;
margin-left: -7px;
margin-top: -7px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 0 18px rgba(255,255,255,0.26);
opacity: 0.72;
}
.node {
position: absolute;
left: 0;
top: 0;
transform-style: preserve-3d;
backface-visibility: hidden;
will-change: transform, opacity;
pointer-events: none;
transform-origin: center center;
}
.card {
display: flex;
align-items: center;
justify-content: center;
transform-style: preserve-3d;
backface-visibility: hidden;
white-space: nowrap;
border-radius: 18px;
overflow: hidden;
}
.label-card {
width: var(--label-w);
height: var(--label-h);
border: 1px solid rgba(255,255,255,0.14);
background:
linear-gradient(180deg, rgba(255,255,255,0.12), rgba(255,255,255,0.03)),
linear-gradient(90deg, rgba(25, 37, 58, 0.90), rgba(10, 15, 26, 0.62));
color: #f4f8ff;
font-size: 19px;
font-weight: 800;
letter-spacing: 0.08em;
text-shadow: 0 1px 0 rgba(0,0,0,0.55);
box-shadow:
0 18px 30px rgba(0,0,0,0.32),
inset 0 1px 0 rgba(255,255,255,0.12),
var(--glow);
position: relative;
}
.label-card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.09), transparent);
opacity: 0.18;
}
.label-card.active {
border-color: rgba(146, 190, 255, 0.66);
background:
linear-gradient(180deg, rgba(146, 190, 255, 0.28), rgba(255,255,255,0.04)),
linear-gradient(90deg, rgba(33, 58, 96, 0.98), rgba(10, 15, 26, 0.72));
box-shadow:
0 22px 38px rgba(0,0,0,0.38),
0 0 30px rgba(146, 190, 255, 0.35),
inset 0 1px 0 rgba(255,255,255,0.16);
}
.divider-card {
width: var(--divider-w);
height: var(--divider-h);
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.08);
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
color: rgba(255,255,255,0.92);
font-size: 20px;
font-weight: 800;
letter-spacing: 0.3em;
text-shadow: 0 0 8px rgba(255,255,255,0.20);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.divider-card img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
image-rendering: pixelated;
}
.divider-card.fallback::before {
content: "*";
}
.title-panel {
position: absolute;
left: 50%;
top: 71%;
transform: translate(-50%, -50%) translateZ(140px);
width: min(620px, 82vw);
padding: 16px 20px;
border: 1px solid rgba(255,255,255,0.10);
border-radius: 20px;
background: linear-gradient(180deg, rgba(5,10,18,0.70), rgba(5,10,18,0.34));
box-shadow: 0 18px 44px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
backdrop-filter: blur(6px);
text-align: center;
z-index: 2;
}
.current-label {
font-size: clamp(24px, 3vw, 40px);
font-weight: 900;
color: #f6faff;
text-transform: uppercase;
letter-spacing: 0.10em;
text-shadow: 0 0 18px rgba(137,184,255,0.24);
}
.footer {
position: absolute;
left: 3vw;
bottom: 3vh;
z-index: 5;
color: rgba(233,240,255,0.50);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
user-select: none;
}
.footer strong {
color: rgba(233,240,255,0.82);
font-weight: 700;
}
@media (max-width: 760px) {
:root {
--perspective: 950px;
--orbit-x: 220px;
--orbit-y: 32px;
--orbit-z: 110px;
--label-w: 154px;
--label-h: 46px;
--divider-w: 92px;
--divider-h: 26px;
}
.current-label { font-size: 26px; }
.title-panel { top: 73%; }
.label-card { font-size: 15px; }
}
</style>
</head>
<body>
<div class="app" id="app" tabindex="0" aria-label="Demo One style orbit menu">
<div class="scanlines"></div>
<div class="vignette"></div>
<div class="hud">
DEMO ONE-INSPIRED ORBIT MENU<br>
ARROW LEFT / RIGHT TO SWITCH ITEMS
</div>
<div class="scene">
<div class="stage">
<div class="orbit-anchor" id="orbitAnchor">
<div class="center-dot"></div>
<div id="orbit"></div>
</div>
<div class="title-panel">
<div class="current-label" id="currentLabel">Games</div>
</div>
</div>
</div>
<div class="footer">
<strong>Hint:</strong> divider fallbacks use a star until you add an image to <code>dividerAssets</code>.
</div>
</div>
<script>
const menuItems = ["Games", "Videos", "Reviews"];
const dividerAssets = [null, null, null];
const orbit = document.getElementById('orbit');
const currentLabel = document.getElementById('currentLabel');
const app = document.getElementById('app');
const state = {
activeIndex: 0,
phase: 0,
targetPhase: 0,
autoPhase: 0,
lastTime: performance.now(),
config: {
x: 340,
y: 54,
z: 180,
speed: 16
},
nodes: []
};
function readConfig() {
const cs = getComputedStyle(document.documentElement);
state.config.x = parseFloat(cs.getPropertyValue('--orbit-x')) || state.config.x;
state.config.y = parseFloat(cs.getPropertyValue('--orbit-y')) || state.config.y;
state.config.z = parseFloat(cs.getPropertyValue('--orbit-z')) || state.config.z;
state.config.speed = parseFloat(cs.getPropertyValue('--spin-speed')) || state.config.speed;
}
function createCard(kind, title, index) {
const node = document.createElement('div');
node.className = 'node';
node.dataset.kind = kind;
node.dataset.index = String(index);
const card = document.createElement('div');
card.className = 'card ' + (kind === 'label' ? 'label-card' : 'divider-card fallback');
if (kind === 'label') {
card.textContent = title;
} else {
const asset = dividerAssets[index % dividerAssets.length];
if (asset) {
card.classList.remove('fallback');
const img = document.createElement('img');
img.src = asset;
img.alt = '*';
card.appendChild(img);
} else {
card.textContent = '*';
}
}
node.appendChild(card);
orbit.appendChild(node);
state.nodes.push({ node, card, kind, index });
}
// Visible sequence around the orbit: label, divider, label, divider, label, divider.
for (let i = 0; i < menuItems.length; i++) {
createCard('label', menuItems[i], i);
createCard('divider', '', i);
}
function wrapDeg(deg) {
return ((deg % 360) + 360) % 360;
}
function setTitle() {
currentLabel.textContent = menuItems[state.activeIndex];
state.nodes.forEach(n => {
if (n.kind === 'label') n.card.textContent = menuItems[n.index];
});
}
function transform3d(x, y, z, rx, ry, rz, sx, sy, sz) {
const m = new DOMMatrix();
m.translateSelf(x, y, z);
m.rotateSelf(rx, ry, rz);
m.scaleSelf(sx, sy, sz);
return m.toString();
}
function render() {
const count = state.nodes.length;
const step = 360 / count;
const labelStep = 360 / menuItems.length;
const centerLabel = Math.round(wrapDeg(-state.phase) / labelStep) % menuItems.length;
for (let i = 0; i < count; i++) {
const item = state.nodes[i];
const isDivider = item.kind === 'divider';
const offset = isDivider ? step * 0.5 : 0;
const aDeg = i * step + state.phase + offset;
const a = aDeg * Math.PI / 180;
const x = Math.sin(a) * state.config.x;
const y = Math.sin(a * 0.5) * state.config.y;
const z = Math.cos(a) * state.config.z;
// Keep everything visible: never let opacity fall too low.
const depth = (z + state.config.z) / (state.config.z * 2);
const opacity = 0.72 + depth * 0.28;
const scale = isDivider ? (0.95 + depth * 0.08) : (0.94 + depth * 0.10);
// 3D globe bend and facing.
const yaw = -Math.sin(a) * 56;
const pitch = Math.sin(a) * 14;
const roll = Math.sin(a * 0.72) * 8;
const squashX = isDivider ? 1.08 : 0.96;
const squashY = 1.0;
item.node.style.transform = transform3d(x, y, z, pitch, yaw, roll, scale * squashX, scale * squashY, 1);
item.node.style.opacity = opacity.toFixed(2);
item.node.style.zIndex = String(Math.round(depth * 1000) + (isDivider ? 0 : 100));
// Counter-rotate the face a little so the text curves with the orbit but remains readable.
const fx = 0;
const fy = 0;
const fz = 0;
const faceYaw = -yaw * 0.55;
const facePitch = -pitch * 0.35;
const faceRoll = -roll * 0.15;
item.card.style.transform = transform3d(fx, fy, fz, facePitch, faceYaw, faceRoll, 1, 1, 1);
item.node.classList.toggle('active', item.kind === 'label' && item.index === centerLabel);
}
}
function move(delta) {
state.activeIndex = (state.activeIndex + delta + menuItems.length) % menuItems.length;
state.targetPhase -= delta * (360 / menuItems.length);
setTitle();
}
function tick(now) {
const dt = Math.min(0.033, (now - state.lastTime) / 1000);
state.lastTime = now;
state.autoPhase -= state.config.speed * dt;
const desired = state.targetPhase + state.autoPhase;
const ease = 1 - Math.pow(0.0001, dt);
state.phase += (desired - state.phase) * ease;
render();
requestAnimationFrame(tick);
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
move(-1);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
move(1);
}
});
window.addEventListener('load', () => {
readConfig();
setTitle();
app.focus();
render();
requestAnimationFrame(tick);
});
window.addEventListener('resize', () => {
readConfig();
render();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment