用数学精度实现超椭圆头像组件(React + TypeScript)

237 阅读3分钟

在日常前端开发中,「头像」通常有两种常见形态:圆形圆角矩形。不过最近,设计团队提出了一个更有趣的需求:

头像形态要求使用“超椭圆”。

首先来对比一下这几种椭圆形态

不同椭圆形态.png

为什么不用 border-radius?

图中的圆角矩形虽然我设置了 border-radius: 15px,在视觉上和超椭圆有些接近,但本质上 CSS 的 border-radius 只是对固定比例的椭圆或圆角矩形进行简单裁剪,无法真正还原超椭圆那种 “自然过渡”“平滑贴边” 的曲线特性。想要更符合现代设计语言的视觉要求,还是需要用数学意义上的超椭圆曲线(Superellipse)来实现。

超椭圆数学公式

超椭圆是一种二维平面曲线,它的基本方程:

image.png

参数说明:

参数含义
a水平方向半径(width/2)
b垂直方向半径(height/2)
n曲线指数,控制圆角弯曲程度

不同 n 值对比:

n效果
2普通椭圆
3~4超椭圆(常用 UI 视觉最佳区间)
>5接近正方形或矩形,角更硬

超椭圆不同n值的对比.png

实现思路:用 SVG ClipPath 精确裁剪

为了兼顾性能和跨浏览器兼容性,我采用了 SVG Path + clipPath 的方案:

  • 用 JavaScript 动态生成超椭圆路径点
  • <clipPath> 裁剪真实的 <image> 标签
  • 支持任意宽高、任意 n 曲线指数、任意图片源

相比 CSS 的 clip-path: path(),这种方式浏览器兼容性更好,也避免了图片渲染缺失问题。

核心实现代码(React + TypeScript)

import React from 'react';

interface SuperellipseAvatarProps extends React.SVGAttributes<SVGSVGElement> {
  /**
   * 大小(宽高相同)
   * @defualt 48
   */
  size?: number;
  /**
   * 用户头像
   */
  src?: string;
  /**
   * 超椭圆指数,越小越圆
   * @default 3
   */
  n?: number;
}

/**
 * 根据超椭圆公式,生成 SVG Path 数据字符串
 *
 * 超椭圆方程:
 *   |x/a|^n + |y/b|^n = 1
 *
 * @param a 半径 a,对应水平方向宽度的一半 (width / 2)
 * @param b 半径 b,对应垂直方向高度的一半 (height / 2)
 * @param n 超椭圆指数,控制曲线弯曲程度,n=2 为普通椭圆,n 越大角越硬朗
 * @param steps 曲线点采样精度,点数越多越平滑,默认 64
 * @returns 返回一个可用于 <path d="..."> 的 SVG Path 字符串
 */
function generateSuperellipsePath(a: number, b: number, n: number, steps = 64): string {
  const points: string[] = [];

  for (let i = 0; i <= steps; i++) {
    // 当前角度 theta,等分整个圆周
    const theta = (Math.PI * 2 * i) / steps;

    // 对应角度的 cos 和 sin 值
    const cos = Math.cos(theta);
    const sin = Math.sin(theta);

    // 超椭圆公式计算当前点 (x, y)
    const x = Math.sign(cos) * Math.pow(Math.abs(cos), 2 / n) * a;
    const y = Math.sign(sin) * Math.pow(Math.abs(sin), 2 / n) * b;

    // 平移坐标系,使 path 落在 (0,0) 到 (width, height) 区间
    points.push(`${x + a},${y + b}`);
  }

  // 用 'M' 开头,再用 'L' 连线所有点,最后闭合路径 'Z'
  return `M${points.join(' L ')} Z`;
}

export const SuperellipseAvatar: React.FC<SuperellipseAvatarProps> = ({
  size = 48,
  src,
  n = 3.5,
  ...restProps
}) => {
  const a = size / 2;
  const b = size / 2;
  const pathData = generateSuperellipsePath(a, b, n);
  const clipId = React.useMemo(() => `clip-${Math.random().toString(36).substring(2, 9)}`, []);

  return (
    <svg
      width={size}
      height={size}
      viewBox={`0 0 ${size} ${size}`}
      {...restProps}
    >
      <defs>
        <clipPath id={clipId}>
          <path d={pathData} />
        </clipPath>
      </defs>

      <image
        href={src || DEFAULT_AVATAR}
        width={size}
        height={size}
        preserveAspectRatio="xMidYMid slice"
        clipPath={`url(#${clipId})`}
      />
    </svg>
  );
};

可配置参数

参数类型说明
sizenumber图片尺寸
srcstring图片 URL
nnumber超椭圆指数,默认 3.5

使用示例:

<SuperellipseAvatar size={60} src="/avatar.jpg" />
<SuperellipseAvatar size={40} src="/user.png" n={4} />

关于 n=3.5 的选择理由:

  • 3.5 是一个视觉上较为自然的经验值
  • 既不会太圆润,也不会太方正
  • 适合大部分中等尺寸头像,特别是企业应用、社交头像列表等场景
  • 如果需要,你可以自行传入 n=3n=4 等做微调

欢迎各位大佬指教~