返回正文
Are you an LLM? You can read better optimized documentation at /zh-CN/blog/2025-08/SuperellipseAvatar.md for this page in Markdown format
用数学精度实现超椭圆头像组件
在日常前端开发中,「头像」通常有两种常见形态:圆形 和 圆角矩形。不过最近,设计团队提出了一个更有趣的需求:
头像形态要求使用“超椭圆”。
首先来对比一下这几种椭圆形态
为什么不用 border-radius
图中的圆角矩形虽然我设置了 border-radius: 15px
,在视觉上和超椭圆有些接近,但本质上 CSS 的 border-radius
只是对固定比例的椭圆或圆角矩形进行简单裁剪,无法真正还原超椭圆那种 “自然过渡” 、 “平滑贴边” 的曲线特性。想要更符合现代设计语言的视觉要求,还是需要用数学意义上的超椭圆曲线(Superellipse)来实现。
超椭圆数学公式
超椭圆是一种二维平面曲线,它的基本方程:
参数说明:
参数 | 含义 | 备注 |
---|---|---|
a | 水平方向半径(width/2 ) | |
b | 垂直方向半径(height/2 ) | |
n | 曲线指数,控制圆角弯曲程度 |
不同 n 值对比
n | 效果 | 备注 |
---|---|---|
2 | 普通椭圆 | |
3~4 | 超椭圆(常用 UI 视觉最佳区间) | |
>5 | 接近正方形或矩形,角更硬 |
实现思路:用 SVG ClipPath 精确裁剪
为了兼顾性能和跨浏览器兼容性,我采用了 SVG Path + clipPath 的方案:
- 用 JavaScript 动态生成超椭圆路径点
- 用
<clipPath>
裁剪真实的<image>
标签 - 支持任意宽高、任意 n 曲线指数、任意图片源
相比 CSS 的 clip-path: path()
,这种方式浏览器兼容性更好,也避免了图片渲染缺失问题。
核心实现代码
js
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>
);
};
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
js
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>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
可配置参数
参数 | 类型 | 说明 |
---|---|---|
size | number | 图片尺寸 |
src | string | 图片 URL |
n | number | 超椭圆指数,默认 3.5 |
使用示例
jsx
<SuperellipseAvatar :size="80" />
1
jsx
<SuperellipseAvatar size={60} src="/avatar.jpg" />
<SuperellipseAvatar size={40} src="/user.png" n={4} />
1
2
2
关于 n=3.5 的选择理由
- 3.5 是一个视觉上较为自然的经验值
- 既不会太圆润,也不会太方正
- 适合大部分中等尺寸头像,特别是企业应用、社交头像列表等场景
- 如果需要,你可以自行传入
n=3
、n=4
等做微调
VN/A |
本站访客数
--次 本站总访问量
--人次