← back to Model Arena

data/artifacts/176d578251e3/gpt.html

614 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Radial Spring Menu</title>
<style>
  :root {
    --bg: #050712;
    --btn-bg: #111827;
    --btn-bg-active: #1f2937;
    --accent: #3b82f6;
    --accent-soft: rgba(59, 130, 246, 0.22);
    --shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.5);
    --size-center: 72px;
    --size-item: 46px;
  }

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  body {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(circle at top, #020617 0, #020617 40%, #020617 60%, #000 100%);
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    color: #e5e7eb;
    overflow: hidden;
  }

  .radial-menu-wrapper {
    position: relative;
    width: 260px;
    height: 260px;
    display: flex;
    align-items: center;
    justify-content: center;
    filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.6));
  }

  .halo {
    position: absolute;
    width: 210px;
    height: 210px;
    border-radius: 50%;
    background:
      radial-gradient(circle at 30% 15%, rgba(96, 165, 250, 0.09), transparent 55%),
      radial-gradient(circle at 75% 85%, rgba(129, 140, 248, 0.09), transparent 55%);
    opacity: 0;
    transform: scale(0.85);
    transition:
      opacity 280ms ease,
      transform 280ms cubic-bezier(0.18, 0.89, 0.32, 1.28);
    pointer-events: none;
  }

  .radial-menu-wrapper.open .halo {
    opacity: 1;
    transform: scale(1);
  }

  .halo::before {
    content: "";
    position: absolute;
    inset: 13%;
    border-radius: inherit;
    border: 1px dashed rgba(148, 163, 184, 0.35);
    border-top-color: rgba(59, 130, 246, 0.7);
    border-left-color: rgba(59, 130, 246, 0.35);
    animation: orbit 7s linear infinite;
    opacity: 0.7;
  }

  @keyframes orbit {
    to {
      transform: rotate(360deg);
    }
  }

  .spring-stem {
    position: absolute;
    width: 2px;
    bottom: 50%;
    top: 50%;
    left: 50%;
    pointer-events: none;
    transform-origin: 50% 100%;
  }

  .spring-stem-line {
    position: absolute;
    width: 100%;
    height: 0;
    background: linear-gradient(to top, rgba(148, 163, 184, 0.1), rgba(59, 130, 246, 0.7));
    border-radius: 999px;
    transform-origin: 50% 100%;
    opacity: 0;
  }

  .radial-menu-wrapper.open .spring-stem-line {
    opacity: 0.8;
  }

  .radial-menu {
    position: relative;
    width: 100%;
    height: 100%;
  }

  .center-button {
    position: absolute;
    left: 50%;
    top: 50%;
    width: var(--size-center);
    height: var(--size-center);
    border-radius: 999px;
    background: radial-gradient(circle at 30% 0%, rgba(59, 130, 246, 0.25), transparent 60%) var(--btn-bg);
    color: #e5e7eb;
    border: 1px solid rgba(148, 163, 184, 0.35);
    box-shadow: var(--shadow-soft);
    transform: translate(-50%, -50%) scale(1);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    outline: none;
    position: absolute;
    transition:
      transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1),
      box-shadow 180ms ease,
      background 200ms ease,
      border-color 200ms ease;
    overflow: hidden;
  }

  .center-button::before {
    content: "";
    position: absolute;
    inset: -40%;
    background: radial-gradient(circle, rgba(59, 130, 246, 0.2), transparent 65%);
    opacity: 0;
    transform: scale(0.6);
    transition:
      opacity 220ms ease,
      transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
  }

  .center-button:focus-visible {
    box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9), 0 0 0 3px rgba(59, 130, 246, 0.7);
  }

  .center-button:hover {
    transform: translate(-50%, -50%) scale(1.05);
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.8);
    border-color: rgba(59, 130, 246, 0.85);
  }

  .center-button:active {
    transform: translate(-50%, -50%) scale(0.96);
    box-shadow: 0 8px 22px rgba(15, 23, 42, 0.9);
  }

  .radial-menu-wrapper.open .center-button {
    background: radial-gradient(circle at 30% 0%, rgba(59, 130, 246, 0.3), transparent 60%) var(--btn-bg-active);
  }

  .radial-menu-wrapper.open .center-button::before {
    opacity: 1;
    transform: scale(1);
  }

  .center-icon {
    position: relative;
    width: 26px;
    height: 26px;
  }

  .center-icon-line {
    position: absolute;
    left: 4px;
    right: 4px;
    height: 2px;
    border-radius: 999px;
    background: linear-gradient(90deg, #60a5fa, #a855f7);
    box-shadow: 0 0 6px rgba(37, 99, 235, 0.6);
    transform-origin: center;
    transition:
      transform 280ms cubic-bezier(0.19, 1, 0.22, 1),
      opacity 180ms ease;
  }

  .center-icon-line:nth-child(1) {
    top: 8px;
  }

  .center-icon-line:nth-child(2) {
    top: 12px;
  }

  .center-icon-line:nth-child(3) {
    top: 16px;
  }

  .radial-menu-wrapper.open .center-icon-line:nth-child(1) {
    transform: translateY(4px) rotate(45deg);
  }

  .radial-menu-wrapper.open .center-icon-line:nth-child(2) {
    opacity: 0;
    transform: scaleX(0.2);
  }

  .radial-menu-wrapper.open .center-icon-line:nth-child(3) {
    transform: translateY(-4px) rotate(-45deg);
  }

  .items-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }

  .menu-item {
    position: absolute;
    width: var(--size-item);
    height: var(--size-item);
    border-radius: 999px;
    background: radial-gradient(circle at 30% 0%, rgba(56, 189, 248, 0.35), transparent 60%) var(--btn-bg);
    border: 1px solid rgba(148, 163, 184, 0.45);
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.95);
    transform-origin: center;
    transform: translate(-50%, -50%) scale(0.2);
    opacity: 0;
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #e5e7eb;
    transition:
      transform 210ms ease-out,
      opacity 170ms ease-out,
      background 160ms ease,
      border-color 160ms ease,
      box-shadow 160ms ease;
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
  }

  .menu-item::before {
    content: "";
    position: absolute;
    inset: 4px;
    border-radius: inherit;
    background: radial-gradient(circle at 30% 0%, rgba(59, 130, 246, 0.12), rgba(15, 23, 42, 0.9));
    opacity: 0.9;
  }

  .menu-item-inner {
    position: relative;
    z-index: 1;
    display: flex;
    transform: scale(1);
    transition: transform 160ms cubic-bezier(0.17, 0.89, 0.32, 1.49);
  }

  .menu-item svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: #bfdbfe;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .radial-menu-wrapper.open .menu-item {
    pointer-events: auto;
  }

  .radial-menu-wrapper.open .menu-item[data-state="active"] {
    transform: translate(calc(var(--tx, 0px) - 50%), calc(var(--ty, 0px) - 50%)) scale(1);
    opacity: 1;
    transition-timing-function: cubic-bezier(0.17, 0.89, 0.32, 1.49);
  }

  .radial-menu-wrapper:not(.open) .menu-item {
    transform: translate(-50%, -50%) scale(0.2);
    opacity: 0;
  }

  .menu-item:hover {
    background: radial-gradient(circle at 30% 0%, rgba(96, 165, 250, 0.4), transparent 60%) #020617;
    border-color: rgba(129, 140, 248, 0.9);
    box-shadow: 0 16px 40px rgba(15, 23, 42, 1);
  }

  .menu-item:hover .menu-item-inner {
    transform: scale(1.12) translateY(-1px);
  }

  .menu-item:active .menu-item-inner {
    transform: scale(0.94) translateY(0);
  }

  .menu-item:active {
    box-shadow: 0 7px 16px rgba(15, 23, 42, 1);
  }

  .menu-item:focus-visible {
    outline: none;
    box-shadow:
      0 0 0 1px rgba(15, 23, 42, 0.9),
      0 0 0 3px rgba(96, 165, 250, 0.9),
      0 12px 30px rgba(15, 23, 42, 1);
  }

  .ripple {
    position: absolute;
    inset: -8px;
    border-radius: inherit;
    border: 1px solid rgba(59, 130, 246, 0.4);
    opacity: 0;
    transform: scale(0.6);
    pointer-events: none;
  }

  .ripple.show {
    animation: ripple-out 420ms ease-out forwards;
  }

  @keyframes ripple-out {
    0% {
      opacity: 0.4;
      transform: scale(0.6);
    }
    75% {
      opacity: 0.12;
      transform: scale(1.18);
    }
    100% {
      opacity: 0;
      transform: scale(1.34);
    }
  }

  .tooltip {
    position: fixed;
    padding: 4px 8px;
    border-radius: 999px;
    font-size: 11px;
    background: rgba(15, 23, 42, 0.96);
    border: 1px solid rgba(148, 163, 184, 0.55);
    color: #e5e7eb;
    box-shadow: 0 10px 30px rgba(15, 23, 42, 0.9);
    pointer-events: none;
    opacity: 0;
    transform: translateY(4px);
    transition:
      opacity 120ms ease-out,
      transform 120ms ease-out;
    z-index: 50;
    white-space: nowrap;
  }

  .tooltip::before {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -4px;
    transform: translateX(-50%);
    border-width: 4px 4px 0 4px;
    border-style: solid;
    border-color: rgba(148, 163, 184, 0.8) transparent transparent transparent;
  }

  .tooltip.show {
    opacity: 1;
    transform: translateY(0);
  }

  .ring-glow {
    position: absolute;
    width: 120px;
    height: 120px;
    border-radius: 999px;
    background: radial-gradient(circle, rgba(37, 99, 235, 0.7), transparent 60%);
    opacity: 0.35;
    filter: blur(20px);
    pointer-events: none;
    transform: translate(-50%, calc(-50% + 28px)) scale(1);
  }

  @media (prefers-reduced-motion: reduce) {
    * {
      scroll-behavior: auto !important;
      animation-duration: 0.001ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.001ms !important;
    }
  }
</style>
</head>
<body>
<div class="radial-menu-wrapper" id="radialWrapper" aria-label="Radial quick actions">
  <div class="ring-glow"></div>
  <div class="halo"></div>
  <div class="spring-stem">
    <div class="spring-stem-line" id="springStem"></div>
  </div>
  <div class="radial-menu" aria-hidden="false">
    <button class="center-button" id="centerButton" aria-expanded="false" aria-label="Toggle quick actions" type="button">
      <div class="center-icon" aria-hidden="true">
        <div class="center-icon-line"></div>
        <div class="center-icon-line"></div>
        <div class="center-icon-line"></div>
      </div>
      <span class="ripple" aria-hidden="true"></span>
    </button>
    <div class="items-layer" id="itemsLayer"></div>
  </div>
</div>

<div class="tooltip" id="tooltip" role="tooltip"></div>

<script>
(function () {
  const wrapper = document.getElementById('radialWrapper');
  const centerButton = document.getElementById('centerButton');
  const itemsLayer = document.getElementById('itemsLayer');
  const tooltip = document.getElementById('tooltip');
  const springStem = document.getElementById('springStem');

  const buttonsData = [
    { label: 'Home', icon: 'home' },
    { label: 'Search', icon: 'search' },
    { label: 'Notifications', icon: 'bell' },
    { label: 'Profile', icon: 'user' },
    { label: 'Settings', icon: 'settings' },
    { label: 'Favorites', icon: 'star' }
  ];

  function iconSvg(name) {
    switch (name) {
      case 'home':
        return '<svg viewBox="0 0 24 24"><path d="M4 11.5L12 4l8 7.5"></path><path d="M6.5 10.5V19a1 1 0 0 0 1 1H11v-5a1 1 0 0 1 1-1h1.5a1 1 0 0 1 1 1v5H16.5a1 1 0 0 0 1-1v-8.5"></path></svg>';
      case 'search':
        return '<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="5"></circle><path d="M15 15l3.5 3.5"></path></svg>';
      case 'bell':
        return '<svg viewBox="0 0 24 24"><path d="M18 15V11a6 6 0 0 0-12 0v4"></path><path d="M5 15h14"></path><path d="M10 18a2 2 0 0 0 4 0"></path></svg>';
      case 'user':
        return '<svg viewBox="0 0 24 24"><circle cx="12" cy="9" r="3.2"></circle><path d="M6.4 18.5A5.9 5.9 0 0 1 12 15a5.9 5.9 0 0 1 5.6 3.5"></path></svg>';
      case 'settings':
        return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.2"></circle><path d="M19 12.9a7 7 0 0 0 0-1.8l2-1.4-1.8-3.1-2.3.7a6.9 6.9 0 0 0-1.6-.9l-.4-2.4h-3.6l-.4 2.4a6.9 6.9 0 0 0-1.6.9l-2.3-.7L3 9.7l2 1.4a7 7 0 0 0 0 1.8l-2 1.4 1.8 3.1 2.3-.7a6.9 6.9 0 0 0 1.6.9l.4 2.4h3.6l.4-2.4a6.9 6.9 0 0 0 1.6-.9l2.3.7 1.8-3.1z"></path></svg>';
      case 'star':
        return '<svg viewBox="0 0 24 24"><path d="M12 4.2l1.97 4 4.43.64-3.2 3.1.76 4.42L12 14.9 8.04 16.4l.76-4.42-3.2-3.1 4.43-.64z"></path></svg>';
      default:
        return '';
    }
  }

  function createButtons() {
    const total = buttonsData.length;
    const radius = 96;
    const startAngle = -120;
    const endAngle = -60;
    const angleStep = (endAngle - startAngle) / (total - 1);

    buttonsData.forEach((data, index) => {
      const angleDeg = startAngle + angleStep * index;
      const angleRad = angleDeg * (Math.PI / 180);
      const tx = Math.cos(angleRad) * radius;
      const ty = Math.sin(angleRad) * radius;

      const btn = document.createElement('button');
      btn.className = 'menu-item';
      btn.type = 'button';
      btn.setAttribute('aria-label', data.label);
      btn.dataset.index = index;
      btn.style.setProperty('--tx', tx.toFixed(2) + 'px');
      btn.style.setProperty('--ty', ty.toFixed(2) + 'px');

      const inner = document.createElement('div');
      inner.className = 'menu-item-inner';
      inner.innerHTML = iconSvg(data.icon);
      btn.appendChild(inner);

      const ripple = document.createElement('span');
      ripple.className = 'ripple';
      btn.appendChild(ripple);

      btn.addEventListener('click', () => {
        triggerRipple(ripple);
        flashStemAlong(index, total);
      });

      btn.addEventListener('mouseenter', (e) => {
        showTooltip(data.label, e.currentTarget);
      });

      btn.addEventListener('mouseleave', hideTooltip);
      btn.addEventListener('focus', (e) => {
        showTooltip(data.label, e.currentTarget);
      });
      btn.addEventListener('blur', hideTooltip);

      itemsLayer.appendChild(btn);
    });
  }

  function triggerRipple(el) {
    el.classList.remove('show');
    // force reflow
    void el.offsetWidth;
    el.classList.add('show');
  }

  function showTooltip(text, target) {
    tooltip.textContent = text;
    const rect = target.getBoundingClientRect();
    const tooltipRect = tooltip.getBoundingClientRect();
    const top = rect.top - tooltipRect.height - 10;
    const left = rect.left + rect.width / 2 - tooltipRect.width / 2;
    tooltip.style.top = top + 'px';
    tooltip.style.left = left + 'px';
    tooltip.classList.add('show');
  }

  function hideTooltip() {
    tooltip.classList.remove('show');
  }

  function flashStemAlong(index, total) {
    const baseHeight = 70;
    const variation = 18 * Math.abs((index - (total - 1) / 2) / (total / 2));
    const height = baseHeight + variation;
    springStem.style.height = height + 'px';
    springStem.style.marginBottom = '4px';

    springStem.style.transition =
      'height 260ms cubic-bezier(0.19, 1, 0.22, 1),' +
      'opacity 260ms ease';
    springStem.style.opacity = '1';
    requestAnimationFrame(() => {
      setTimeout(() => {
        springStem.style.height = '0px';
        springStem.style.opacity = '0';
      }, 10);
    });
  }

  function setItemsState(open) {
    const items = Array.from(itemsLayer.querySelectorAll('.menu-item'));
    const stem = springStem;

    if (open) {
      const maxHeight = 110;
      stem.style.height = maxHeight + 'px';
      stem.style.marginBottom = '5px';
      stem.style.transition =
        'height 300ms cubic-bezier(0.19, 1, 0.22, 1),' +
        'opacity 300ms ease';
      stem.style.opacity = '0.95';

      items.forEach((btn, i) => {
        const delay = 30 + i * 30;
        btn.style.transitionDelay = delay + 'ms';
        btn.dataset.state = 'active';
      });

      setTimeout(() => {
        stem.style.height = '80px';
        setTimeout(() => {
          stem.style.height = '0px';
          stem.style.opacity = '0';
        }, 160);
      }, 260);
    } else {
      items.forEach((btn, i) => {
        const delay = (items.length - i - 1) * 20;
        btn.style.transitionDelay = delay + 'ms';
        btn.dataset.state = 'inactive';
      });
      stem.style.height = '0px';
      stem.style.opacity = '0';
    }
  }

  function toggleMenu() {
    const isOpen = wrapper.classList.toggle('open');
    centerButton.setAttribute('aria-expanded', String(isOpen));
    setItemsState(isOpen);

    const ripple = centerButton.querySelector('.ripple');
    triggerRipple(ripple);
  }

  centerButton.addEventListener('click', toggleMenu);

  centerButton.addEventListener('keydown', (e) => {
    if (e.key === ' ' || e.key === 'Enter') {
      e.preventDefault();
      toggleMenu();
    }
  });

  createButtons();
})();
</script>
</body>
</html>