← 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>