← back to Model Arena
data/artifacts/176d578251e3/claude-code.html
303 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>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
display: grid;
place-items: center;
background:
radial-gradient(1200px 700px at 70% 20%, #1b2340 0%, transparent 60%),
radial-gradient(900px 600px at 20% 85%, #0f2b2a 0%, transparent 55%),
#0b0e17;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
.stage {
position: relative;
width: 420px;
height: 420px;
display: grid;
place-items: center;
}
/* soft pulse ring that fires on open */
.pulse {
position: absolute;
left: 50%; top: 50%;
width: 76px; height: 76px;
margin: -38px 0 0 -38px;
border-radius: 50%;
border: 2px solid rgba(124, 140, 255, .55);
opacity: 0;
pointer-events: none;
}
.pulse.go { animation: pulse .7s cubic-bezier(.2,.7,.3,1) forwards; }
@keyframes pulse {
0% { opacity: .8; transform: scale(.6); }
100% { opacity: 0; transform: scale(3.1); }
}
/* ---------- center button ---------- */
.fab {
position: absolute;
left: 50%; top: 50%;
width: 76px; height: 76px;
margin: -38px 0 0 -38px;
border: none;
border-radius: 50%;
cursor: pointer;
z-index: 10;
background: linear-gradient(135deg, #7c8cff 0%, #5f6cf0 45%, #9a6cf0 100%);
box-shadow:
0 10px 30px rgba(95, 108, 240, .45),
0 2px 6px rgba(0,0,0,.4),
inset 0 1px 0 rgba(255,255,255,.35);
display: grid;
place-items: center;
transition: transform .18s cubic-bezier(.3,.7,.4,1.4), box-shadow .3s ease;
outline: none;
}
.fab:hover {
transform: scale(1.07);
box-shadow:
0 14px 40px rgba(95, 108, 240, .6),
0 2px 6px rgba(0,0,0,.4),
inset 0 1px 0 rgba(255,255,255,.35);
}
.fab:active { transform: scale(.93); }
.fab:focus-visible { box-shadow: 0 0 0 3px rgba(124,140,255,.7), 0 10px 30px rgba(95,108,240,.45); }
.fab svg {
width: 30px; height: 30px;
stroke: #fff;
stroke-width: 2.6;
stroke-linecap: round;
transition: transform .45s cubic-bezier(.3,1.4,.4,1);
}
.fab.open svg { transform: rotate(225deg); }
/* ---------- radial items ---------- */
.item {
position: absolute;
left: 50%; top: 50%;
width: 54px; height: 54px;
margin: -27px 0 0 -27px;
pointer-events: none; /* enabled by JS when open */
will-change: transform, opacity;
z-index: 5;
}
.item button {
width: 100%; height: 100%;
border: none;
border-radius: 50%;
cursor: pointer;
display: grid;
place-items: center;
color: #eaeefc;
background: rgba(255,255,255,.09);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,.14);
box-shadow: 0 6px 18px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.18);
transition: transform .18s cubic-bezier(.3,.7,.4,1.6),
background .25s ease, color .25s ease, border-color .25s ease;
outline: none;
}
.item button:hover {
transform: scale(1.22);
background: rgba(124,140,255,.28);
border-color: rgba(160,172,255,.55);
color: #ffffff;
}
.item button:active { transform: scale(1.02); }
.item button:focus-visible { border-color: #aab4ff; }
.item svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
/* tooltip label */
.item .tip {
position: absolute;
left: 50%; bottom: calc(100% + 10px);
transform: translateX(-50%) translateY(6px) scale(.85);
background: rgba(18, 22, 38, .92);
border: 1px solid rgba(255,255,255,.12);
color: #dfe4ff;
font-size: 11px;
letter-spacing: .04em;
padding: 4px 9px;
border-radius: 999px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity .16s ease, transform .2s cubic-bezier(.3,.9,.4,1.4);
}
.item button:hover + .tip {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
.hint {
position: absolute;
bottom: 26px;
left: 0; right: 0;
text-align: center;
color: rgba(210,218,255,.35);
font-size: 12px;
letter-spacing: .12em;
text-transform: uppercase;
user-select: none;
transition: opacity .4s ease;
}
.hint.hide { opacity: 0; }
</style>
</head>
<body>
<div class="stage" id="stage">
<div class="pulse" id="pulse"></div>
<button class="fab" id="fab" aria-label="Toggle menu" aria-expanded="false">
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
</button>
</div>
<div class="hint" id="hint">Tap the button</div>
<script>
(function () {
const ICONS = [
{ label: 'Home', svg: '<path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10.5V20h13v-9.5"/>' },
{ label: 'Search', svg: '<circle cx="11" cy="11" r="6.5"/><path d="m16.2 16.2 4.3 4.3"/>' },
{ label: 'Favorite', svg: '<path d="M12 20.5s-7.5-4.6-9.3-9C1.4 8 3.2 4.8 6.4 4.8c2 0 3.6 1.1 4.6 2.8h2c1-1.7 2.6-2.8 4.6-2.8 3.2 0 5 3.2 3.7 6.7-1.8 4.4-9.3 9-9.3 9Z" transform="scale(.92) translate(1 1)"/>' },
{ label: 'Camera', svg: '<rect x="3" y="7" width="18" height="13" rx="2.5"/><path d="M8.5 7 10 4.5h4L15.5 7"/><circle cx="12" cy="13.2" r="3.6"/>' },
{ label: 'Music', svg: '<path d="M9 18.5V6l11-2v12.5"/><circle cx="6.6" cy="18.6" r="2.6"/><circle cx="17.6" cy="16.6" r="2.6"/>' },
{ label: 'Settings', svg: '<circle cx="12" cy="12" r="3.2"/><path d="M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1"/>' }
];
const N = ICONS.length;
const RADIUS = 138;
const START = -195; // degrees — arc fans over the top
const END = 15;
const STAGGER = 45; // ms between item launches
const K = 190, C = 13.5; // spring stiffness / damping (underdamped = bouncy)
const stage = document.getElementById('stage');
const fab = document.getElementById('fab');
const pulse = document.getElementById('pulse');
const hint = document.getElementById('hint');
const items = ICONS.map((ic, i) => {
const angle = (START + (END - START) * (i / (N - 1))) * Math.PI / 180;
const el = document.createElement('div');
el.className = 'item';
el.innerHTML =
'<button aria-label="' + ic.label + '"><svg viewBox="0 0 24 24">' + ic.svg + '</svg></button>' +
'<span class="tip">' + ic.label + '</span>';
stage.appendChild(el);
el.querySelector('button').addEventListener('click', (e) => {
// playful acknowledgment: quick squash on the tapped item, then retract
e.currentTarget.style.transform = 'scale(.8)';
setTimeout(() => { e.currentTarget.style.transform = ''; }, 120);
setTimeout(toggle, 160);
});
return {
el,
tx: Math.cos(angle) * RADIUS,
ty: Math.sin(angle) * RADIUS,
p: 0, v: 0, // spring progress + velocity
target: 0,
activateAt: 0 // timestamp when this item's spring engages
};
});
let open = false;
let rafId = null;
let last = 0;
function toggle() {
open = !open;
fab.classList.toggle('open', open);
fab.setAttribute('aria-expanded', String(open));
hint.classList.add('hide');
const now = performance.now();
items.forEach((it, i) => {
const order = open ? i : (N - 1 - i); // retract in reverse order
it.activateAt = now + order * STAGGER;
});
if (open) {
pulse.classList.remove('go');
void pulse.offsetWidth; // restart animation
pulse.classList.add('go');
}
if (rafId === null) { last = now; rafId = requestAnimationFrame(frame); }
}
function frame(now) {
let dt = Math.min((now - last) / 1000, 1 / 30);
last = now;
let settled = true;
for (const it of items) {
if (now >= it.activateAt) it.target = open ? 1 : 0;
// semi-implicit Euler spring integration
const a = K * (it.target - it.p) - C * it.v;
it.v += a * dt;
it.p += it.v * dt;
if (Math.abs(it.target - it.p) > 0.0008 || Math.abs(it.v) > 0.0008 || (open ? it.target !== 1 : it.target !== 0)) {
settled = false;
}
const p = it.p;
const clamped = Math.max(0, Math.min(1.15, p));
const x = it.tx * p;
const y = it.ty * p;
const rot = (1 - Math.min(1, clamped)) * -90; // slight spin-in as it flies out
const scale = Math.max(0.001, clamped);
it.el.style.transform = 'translate(' + x.toFixed(2) + 'px,' + y.toFixed(2) + 'px) rotate(' + rot.toFixed(1) + 'deg) scale(' + scale.toFixed(3) + ')';
it.el.style.opacity = Math.max(0, Math.min(1, p * 1.4)).toFixed(3);
it.el.style.pointerEvents = (open && p > 0.6) ? 'auto' : 'none';
}
if (settled) {
// snap exactly to rest so hover transforms stay crisp
for (const it of items) {
it.p = it.target; it.v = 0;
it.el.style.transform = 'translate(' + (it.tx * it.p) + 'px,' + (it.ty * it.p) + 'px) rotate(0deg) scale(' + Math.max(0.001, it.p) + ')';
it.el.style.opacity = it.p;
}
rafId = null;
return;
}
rafId = requestAnimationFrame(frame);
}
// initialize hidden state
for (const it of items) {
it.el.style.transform = 'translate(0px,0px) scale(0.001)';
it.el.style.opacity = '0';
}
fab.addEventListener('click', toggle);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && open) toggle();
});
})();
</script>
</body>
</html>