---
URL: "/zh-CN/blog/2025-08/SuperellipseAvatar.html"
LLMS_URL: "/zh-CN/blog/2025-08/SuperellipseAvatar.md"
lastUpdated: true
commentabled: true
recommended: true
title: "用数学精度实现超椭圆头像组件"
description: "用数学精度实现超椭圆头像组件"
date:

pageClass: "blog-page-class"
cover: "/covers/html5.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: 用数学精度实现超椭圆头像组件
description: 用数学精度实现超椭圆头像组件
date: 2025-08-21 13:45:00 
pageClass: blog-page-class
cover: /covers/html5.svg
---

在日常前端开发中，「头像」通常有两种常见形态：**圆形** 和 **圆角矩形**。不过最近，设计团队提出了一个更有趣的需求：

> 头像形态要求使用“超椭圆”。

## 首先来对比一下这几种椭圆形态 ##

![](/images/SuperellipseAvatar1.jpg){data-zoomable}

## 为什么不用 border-radius ##

图中的圆角矩形虽然我设置了 `border-radius: 15px`，在视觉上和超椭圆有些接近，但本质上 CSS 的 `border-radius` 只是对固定比例的椭圆或圆角矩形进行简单裁剪，无法真正还原超椭圆那种 “*自然过渡*” 、 “*平滑贴边*” 的曲线特性。想要更符合现代设计语言的视觉要求，还是需要用数学意义上的超椭圆曲线（Superellipse）来实现。

## 超椭圆数学公式 ##

超椭圆是一种二维平面曲线，它的基本方程：

![](/images/SuperellipseAvatar3.jpg){data-zoomable}

参数说明：

| 参数  |  含义  |  备注  |
| :-------: | :---------: | :--------: |
| a | 水平方向半径（`width/2`） |  |
| b | 垂直方向半径（`height/2`） |  |
| n | 曲线指数，控制圆角弯曲程度 |  |

### 不同 n 值对比 ###

| n  |  效果  |  备注  |
| :-------: | :---------: | :--------: |
| 2 | 普通椭圆 |  |
| 3~4 | 超椭圆（常用 UI 视觉最佳区间） |  |
| >5 | 接近正方形或矩形，角更硬 |  |

![](/images/SuperellipseAvatar2.jpg){data-zoomable}

## 实现思路：用 SVG ClipPath 精确裁剪 ##

为了兼顾性能和跨浏览器兼容性，我采用了 *SVG Path + clipPath* 的方案：

1. 用 JavaScript 动态生成超椭圆路径点
2. 用 `<clipPath>` 裁剪真实的 `<image>` 标签
3. 支持任意宽高、任意 n 曲线指数、任意图片源

相比 CSS 的 `clip-path: path()`，这种方式浏览器兼容性更好，也避免了图片渲染缺失问题。

## 核心实现代码 ##

::: code-group

```js $(1#) [vue]
import { defineComponent, computed } from 'vue';

const DEFAULT_AVATAR = '/logo.png';
const EMPTY_AVATAR = "https://via.placeholder.com/100?text=No+Image";

const nValues = [
  { n: 0.5, label: "菱形 (n=0.5)" },
  { n: 1, label: "菱形 (n=1)" },
  { n: 2, label: "椭圆 (n=2)" },
  { n: 3.5, label: "超椭圆 (n=3.5)" },
  { n: 10, label: "方圆形 (n=10)" },
];

interface SuperellipseAvatarProps {
  /**
   * 大小(宽高相同)
   * @default 48
   */
  size?: number;
  /**
   * 用户头像
   */
  src?: string;
  /**
   * 超椭圆指数，越小越圆
   * @default 3
   */
  n?: number;
  /**
   * 其他 SVG 属性
   */
  [key: string]: any;
}

/**
 * 根据超椭圆公式，生成 SVG Path 数据字符串
 */
function generateSuperellipsePath(a: number, b: number, n: number, steps = 64): string {
  const points: string[] = [];

  for (let i = 0; i <= steps; i++) {
    const theta = (Math.PI * 2 * i) / steps;
    const cos = Math.cos(theta);
    const sin = Math.sin(theta);
    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;
    points.push(`${x + a},${y + b}`);
  }

  return `M${points.join(' L ')} Z`;
}

export const SuperellipseAvatar = defineComponent({
  name: "SuperellipseAvatar",
  props: {
    size: {
      type: Number,
      default: 48,
    },
    src: String,
    n: {
      type: Number,
      default: 3.5,
    },
    backgroundColor: {
      type: String,
      default: "#e0e0e0",
    },
  },
  setup(props: SuperellipseAvatarProps) {
    const clipId = computed(
      () => `clip-${Math.random().toString(36).substring(2, 9)}`
    );

    const pathData = computed(() => {
      const a = props.size! / 2;
      const b = props.size! / 2;
      return generateSuperellipsePath(a, b, props.n!);
    });

    function generateSuperellipsePath(a, b, n, steps = 64) {
      const points: string[] = [];
      for (let i = 0; i <= steps; i++) {
        const theta = (Math.PI * 2 * i) / steps;
        const cos = Math.cos(theta);
        const sin = Math.sin(theta);
        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;
        points.push(`${x + a},${y + b}`);
      }
      return `M${points.join(" L ")} Z`;
    }

    return () => {
      const { size, src, n, ...restProps } = props;

      return (
        <svg
          fill="red"
          width={size}
          height={size}
          viewBox={`0 0 ${size} ${size}`}
          {...restProps}
        >
          <defs>
            <clipPath id={clipId.value}>
              <path d={pathData.value} />
            </clipPath>
          </defs>
          {/* 背景矩形 */}
          <rect
            width={props.size}
            height={props.size}
            fill={props.backgroundColor}
            clip-path={`url(#${clipId.value})`}
          />

          {/* 头像图片 */}
          <image
            href={src || DEFAULT_AVATAR}
            width={size}
            height={size}
            preserveAspectRatio="xMidYMid slice"
            clip-path={`url(#${clipId.value})`}
          />
        </svg>
      );
    };
  },
});

```

```js [react]
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>
  );
};
```

:::

## 可配置参数 ##

| 参数  |  类型  |  说明  |
| :-------: | :---------: | :--------: |
| size | number | 图片尺寸 |
| src | string | 图片 URL |
| n | number | 超椭圆指数，默认 3.5 |

## 使用示例 ##

::: code-group

```jsx [vue]
<SuperellipseAvatar :size="80" />
```


```jsx [react]
<SuperellipseAvatar size={60} src="/avatar.jpg" />
<SuperellipseAvatar size={40} src="/user.png" n={4} />
```

:::

## 关于 n=3.5 的选择理由 ##

- 3.5 是一个视觉上较为自然的经验值
- 既不会太圆润，也不会太方正
- 适合大部分中等尺寸头像，特别是企业应用、社交头像列表等场景
- 如果需要，你可以自行传入 `n=3`、`n=4` 等做微调
