← back to Model Arena
data/artifacts/176d578251e3/grok.html
430 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Radial Spring Menu</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0c0e14;
--surface: #161a24;
--accent: #6c5ce7;
--accent-glow: rgba(108, 92, 231, 0.45);
--text: #e8eaf0;
--muted: #8b90a5;
--item-size: 52px;
--center-size: 64px;
}
html, body {
height: 100%;
overflow: hidden;
background: var(--bg);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--text);
}
body {
display: grid;
place-items: center;
background:
radial-gradient(ellipse 80% 60% at 50% 45%, rgba(108, 92, 231, 0.12), transparent 55%),
radial-gradient(ellipse 50% 40% at 70% 70%, rgba(0, 206, 201, 0.06), transparent 50%),
var(--bg);
}
.hint {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
opacity: 0.7;
pointer-events: none;
transition: opacity 0.4s ease;
}
.hint.hide { opacity: 0; }
.menu {
position: relative;
width: var(--center-size);
height: var(--center-size);
}
.center {
position: absolute;
inset: 0;
z-index: 10;
border: none;
border-radius: 50%;
cursor: pointer;
background: linear-gradient(145deg, #7f71f0, #5a4bd1);
color: #fff;
display: grid;
place-items: center;
box-shadow:
0 4px 20px var(--accent-glow),
0 0 0 0 rgba(108, 92, 231, 0.3),
inset 0 1px 0 rgba(255,255,255,0.2);
transition: box-shadow 0.3s ease, transform 0.15s ease;
-webkit-tap-highlight-color: transparent;
outline: none;
}
.center:hover {
box-shadow:
0 6px 28px var(--accent-glow),
0 0 0 6px rgba(108, 92, 231, 0.15),
inset 0 1px 0 rgba(255,255,255,0.25);
}
.center:active { transform: scale(0.94); }
.center svg {
width: 28px;
height: 28px;
transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.menu.open .center svg { transform: rotate(45deg); }
.center::after {
content: "";
position: absolute;
inset: -4px;
border-radius: 50%;
border: 2px solid transparent;
background: linear-gradient(var(--bg), var(--bg)) padding-box,
linear-gradient(135deg, rgba(108,92,231,0.5), rgba(0,206,201,0.3)) border-box;
opacity: 0;
transition: opacity 0.35s ease;
pointer-events: none;
z-index: -1;
}
.menu.open .center::after { opacity: 1; }
.item {
position: absolute;
top: 50%;
left: 50%;
width: var(--item-size);
height: var(--item-size);
margin: calc(var(--item-size) / -2) 0 0 calc(var(--item-size) / -2);
border: none;
border-radius: 50%;
cursor: pointer;
background: var(--surface);
color: var(--text);
display: grid;
place-items: center;
box-shadow:
0 4px 16px rgba(0,0,0,0.35),
inset 0 1px 0 rgba(255,255,255,0.06);
transform: translate(0, 0) scale(0);
opacity: 0;
pointer-events: none;
will-change: transform, opacity;
-webkit-tap-highlight-color: transparent;
outline: none;
transition: background 0.2s ease, box-shadow 0.25s ease, color 0.2s ease;
}
.item::before {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, rgba(255,255,255,0.1), transparent 60%);
pointer-events: none;
}
.item svg {
width: 22px;
height: 22px;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.item:hover {
background: linear-gradient(145deg, #7f71f0, #5a4bd1);
color: #fff;
box-shadow:
0 6px 24px var(--accent-glow),
0 0 0 3px rgba(108, 92, 231, 0.2),
inset 0 1px 0 rgba(255,255,255,0.2);
}
.item:hover svg { transform: scale(1.12); }
.item:active { filter: brightness(0.92); }
.item .tooltip {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(4px);
padding: 5px 10px;
border-radius: 6px;
background: #1e2433;
color: var(--text);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
white-space: nowrap;
opacity: 0;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.item .tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -4px;
border: 4px solid transparent;
border-top-color: #1e2433;
}
.item:hover .tooltip {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255,255,255,0.35);
transform: scale(0);
animation: ripple 0.55s ease-out forwards;
pointer-events: none;
}
@keyframes ripple {
to { transform: scale(2.4); opacity: 0; }
}
.orbit-ring {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border: 1px solid rgba(108, 92, 231, 0.15);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
opacity: 0;
transition: opacity 0.4s ease;
}
.menu.open .orbit-ring { opacity: 1; }
</style>
</head>
<body>
<p class="hint" id="hint">Click the button</p>
<div class="menu" id="menu">
<div class="orbit-ring" id="ring"></div>
<button class="center" id="center" aria-label="Toggle menu" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
</div>
<script>
(function () {
const ITEMS = [
{ label: "Home", icon: '<path d="M3 10.5L12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1v-9.5z"/>' },
{ label: "Search", icon: '<circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/>' },
{ label: "Messages", icon: '<path d="M4 5h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H8l-4 3v-3H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/>' },
{ label: "Favorites", icon: '<path d="M12 3.5l2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 15.8 7.2 18.4l.9-5.4-3.9-3.8 5.4-.8L12 3.5z"/>' },
{ label: "Profile", icon: '<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>' },
{ label: "Settings", icon: '<circle cx="12" cy="12" r="3"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4l1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4m11.4-11.4l1.4-1.4"/>' }
];
const RADIUS = 120;
const ARC_START = -200;
const ARC_END = 20;
const STIFFNESS = 0.14;
const DAMPING = 0.72;
const MASS_STAGGER = 0.012;
const menu = document.getElementById("menu");
const center = document.getElementById("center");
const ring = document.getElementById("ring");
const hint = document.getElementById("hint");
let open = false;
let animating = false;
let raf = null;
const buttons = ITEMS.map((item, i) => {
const btn = document.createElement("button");
btn.className = "item";
btn.setAttribute("aria-label", item.label);
btn.innerHTML =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
item.icon + "</svg>" +
'<span class="tooltip">' + item.label + "</span>";
menu.appendChild(btn);
const t = ITEMS.length === 1 ? 0.5 : i / (ITEMS.length - 1);
const angleDeg = ARC_START + t * (ARC_END - ARC_START);
const angleRad = (angleDeg * Math.PI) / 180;
return {
el: btn,
angle: angleRad,
r: 0,
v: 0,
target: 0,
scale: 0,
sv: 0,
scaleTarget: 0,
opacity: 0,
hoverScale: 1,
hv: 0,
hoverTarget: 1,
delay: i * MASS_STAGGER
};
});
ring.style.width = ring.style.height = RADIUS * 2 + "px";
function springStep(pos, vel, target, k, d) {
const force = -k * (pos - target);
const accel = force;
vel = (vel + accel) * d;
pos += vel;
if (Math.abs(vel) < 0.01 && Math.abs(pos - target) < 0.01) {
return { pos: target, vel: 0, settled: true };
}
return { pos, vel, settled: false };
}
function tick() {
let allSettled = true;
for (let i = 0; i < buttons.length; i++) {
const b = buttons[i];
const k = STIFFNESS - b.delay * 0.3;
const d = DAMPING;
const rs = springStep(b.r, b.v, b.target, k, d);
b.r = rs.pos;
b.v = rs.vel;
if (!rs.settled) allSettled = false;
const ss = springStep(b.scale, b.sv, b.scaleTarget, k + 0.02, d);
b.scale = ss.pos;
b.sv = ss.vel;
if (!ss.settled) allSettled = false;
const hs = springStep(b.hoverScale, b.hv, b.hoverTarget, 0.22, 0.65);
b.hoverScale = hs.pos;
b.hv = hs.vel;
if (!hs.settled) allSettled = false;
const x = Math.cos(b.angle) * b.r;
const y = Math.sin(b.angle) * b.r;
const s = Math.max(0, b.scale) * b.hoverScale;
b.opacity += ((b.scaleTarget > 0.5 ? 1 : 0) - b.opacity) * 0.18;
b.el.style.transform = "translate(" + x.toFixed(2) + "px," + y.toFixed(2) + "px) scale(" + s.toFixed(3) + ")";
b.el.style.opacity = Math.max(0, Math.min(1, b.opacity)).toFixed(3);
b.el.style.pointerEvents = open && b.r > RADIUS * 0.5 ? "auto" : "none";
}
if (!allSettled || animating) {
raf = requestAnimationFrame(tick);
} else {
raf = null;
animating = false;
}
}
function startLoop() {
if (!raf) {
animating = true;
raf = requestAnimationFrame(tick);
}
}
function toggle() {
open = !open;
menu.classList.toggle("open", open);
center.setAttribute("aria-expanded", open ? "true" : "false");
hint.classList.add("hide");
buttons.forEach((b, i) => {
if (open) {
b.target = RADIUS;
b.scaleTarget = 1;
b.v += 0.8 + i * 0.15;
} else {
b.target = 0;
b.scaleTarget = 0;
b.hoverTarget = 1;
b.v -= 0.5;
}
});
startLoop();
}
center.addEventListener("click", function (e) {
spawnRipple(center, e);
toggle();
});
buttons.forEach((b) => {
b.el.addEventListener("mouseenter", () => {
if (!open) return;
b.hoverTarget = 1.18;
startLoop();
});
b.el.addEventListener("mouseleave", () => {
b.hoverTarget = 1;
startLoop();
});
b.el.addEventListener("click", (e) => {
spawnRipple(b.el, e);
b.hoverTarget = 0.9;
startLoop();
setTimeout(() => {
b.hoverTarget = 1.18;
startLoop();
}, 120);
});
});
function spawnRipple(el, e) {
const rect = el.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const ripple = document.createElement("span");
ripple.className = "ripple";
ripple.style.width = ripple.style.height = size + "px";
ripple.style.left = e.clientX - rect.left - size / 2 + "px";
ripple.style.top = e.clientY - rect.top - size / 2 + "px";
el.appendChild(ripple);
ripple.addEventListener("animationend", () => ripple.remove());
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && open) toggle();
});
})();
</script>
</body>
</html>