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