CSS里的sin()和cos(),真就人人喊打?,来,咱们给这俩“数学课代表”找点正事儿干干

3,797字
16–24 分钟
in

CSS 三角函数凭啥成了“最遭人恨”的特性?说实话,看到 State of CSS 2025 的投票结果时,我整个人都惊了。9.1% 的开发者居然“痛恨”三角函数?这得是多大的冤屈啊。作为一个天天跟布局死磕的前端,我觉得是时候给 sin() 和 cos() 这哥俩正名了。咱别一看到数学公式就头疼,今天就用最接地气的方式,把这俩函数从“最遭恨”名单里捞出来,让它们真正成为咱们布局工具箱里的神兵利器。

目录

摘要
CSS 的 sin() 和 cos() 函数并非高深莫测的数学概念,它们本质上是将角度转化为坐标的实用工具。本文从单位圆这一直观模型切入,摒弃复杂的学术定义,直接展示这两个函数在实战中的落地场景。通过圆形菜单、波浪链条和阻尼动画三个案例,一步步拆解如何用它们替代繁琐的绝对定位计算,让布局代码更简洁、更灵活。这些方案不依赖 JavaScript,完全在 CSS 层面实现,适合追求代码优雅的前端开发者和对 CSS 新特性感兴趣的设计师。

用单位圆整明白sin()和cos()是咋回事

咱们先回到原点,聊聊这俩函数到底在算啥。别慌,咱不整那些让人头大的数学推导,只用一个模型:单位圆。想象一个半径为 1 的圆,把它扔到坐标系里。圆上的每个点都能用一个角度来描述,这个角度从 X 轴正方向开始,逆时针转。而 sin() 和 cos() 这俩函数,干的事儿就是帮咱们把这个角度翻译成坐标。

cos(角度) = X 轴坐标
sin(角度) = Y 轴坐标

比如说角度是 0°,点就在 (1, 0);角度是 90°,点就在 (0, 1)。所以你看,只要给定一个角度,这俩函数就能告诉你这个点在圆上的位置。这相当于咱们在数学和布局之间架了一座桥,以后想在页面上画圆、摆波浪,直接用角度就能算出每个元素该待的位置,再也不用硬编码一大堆像素值了。

用sin()和cos()安排一个圆溜溜的菜单

咱们先来整个活儿:把一堆圆点均匀地摆成一个正圆。以前要实现这种效果,得手算每个点的坐标,代码又臭又长。现在用上 sin() 和 cos(),这事儿就简单多了。

第一步,在 HTML 里给每个元素标个号。可以用内联变量来记录它们的位置顺序,这样后面计算角度时就方便了。

<ul style="--total: 8">
  <li style="--i: 0">0</li>
  <li style="--i: 1">1</li>
  <li style="--i: 2">2</li>
  <li style="--i: 3">3</li>
  <li style="--i: 4">4</li>
  <li style="--i: 5">5</li>
  <li style="--i: 6">6</li>
  <li style="--i: 7">7</li>
</ul>

第二步,在 CSS 里算出每个元素的角度。一圈是 360°,所以每个元素之间的夹角就是 360° 除以总数。用每个元素的索引乘以这个步长,就得到了它独有的旋转角度。

li {
  --rotation: calc(360deg / var(--total) * var(--i));
}

第三步,设定半径,然后用 sin() 和 cos() 算出每个元素的最终位置。radius 决定了圆的大小,用三角函数算出坐标后再乘以 radius,就得到了最终的偏移量。

ul {
  --radius: 10rem;
  position: relative;
}
li {
  position: absolute;
  transform: translateX(calc(cos(var(--rotation)) * var(--radius))) 
             translateY(calc(sin(var(--rotation)) * var(--radius)));
}

这个方案有个小细节需要注意,就是定位上下文。因为咱们用的是绝对定位,所以父容器必须设置 position: relative,不然所有元素都会叠在一起跑偏。另外,如果用半圆布局,只需要把总角度改成 180° 就行。如果要做一个从中心点展开的圆形菜单,还可以通过动画改变 –radius 的值,让菜单项像花瓣一样散开。

用sin()和cos()摆出波浪链条

除了画圆,这俩函数在波浪布局上也特别顺手。核心思路是利用三角函数值在 -1 到 1 之间周期性变化的特性,让元素在垂直方向上有规律地起伏。

假设有一串元素排成一行,现在想让它们沿着一条正弦曲线上下浮动。第一步,还是给每个元素标上索引,这次可以不用知道总数,直接根据索引来计算偏移量。

<ul>
  <li style="--i: 0"></li>
  <li style="--i: 1"></li>
  <li style="--i: 2"></li>
  <!-- 随意加,不设上限 -->
</ul>

第二步,用 sin() 函数计算垂直偏移。角度可以用索引乘上一个固定值来控制波形的疏密。比如 60deg 的时候波浪比较平缓,想浪一点就调大角度。

li {
  transform: translateY(calc(sin(60deg * var(--i)) * 50px));
}

这里的关键在于,50px 决定了波浪的高度。这个值可以根据实际需求调整,但要注意,如果元素本身有高度,波浪的幅度最好不要超过元素间距,否则会重叠。

咱们可以玩得更花哨一点,比如做两条波浪链交织在一起,模仿 DNA 双螺旋的结构。这时候就需要两个不同的列表,分别用 sin() 和 cos() 来控制垂直位移。

.principal li {
  transform: translateY(calc(sin(60deg * var(--i)) * 50px));
}
.secondary li {
  transform: translateY(calc(cos(60deg * var(--i)) * 50px));
}

为了让两条链错开,还可以在 secondary 的计算里加上一个额外的偏移角度,比如 +60deg,让两条链的波峰和波谷完美错开。这时候记得用 z-index 区分一下层级,让一条链在上,另一条在下,这样视觉效果更明显。

用cos()模拟阻尼振荡动画

动画里也有 sin() 和 cos() 的用武之地,比如模拟一个有物理感的阻尼振荡效果。普通的来回动画用 animation 的 alternate 就能搞定,但那种慢慢停下来的真实感,就得靠数学公式来实现了。

阻尼振荡的公式大概长这样:振幅 × e^(-阻尼系数 × 时间) × cos(频率 × 时间)。咱们把这个公式搬到 CSS 里,就能做出一个自然衰减的动画。

第一步,注册一个 –progress 变量,作为时间的替代品。用 @property 声明它,让它能被动画驱动。

@property --progress {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

第二步,定义动画,让 –progress 从 0 变到某个值,比如 25。

@keyframes damping {
  from { --progress: 0; }
  to { --progress: 25; }
}

第三步,用 CSS 实现公式。这里把公式拆开写会比较清晰,最后用 transform 应用 X 轴方向的位移。

.circle {
  --damping: 0.3;
  --amplitude: 200px;
  --frequency: 0.8;
  --offset: calc(pi / 2);

  --oscillation: calc(
    (exp(-1 * var(--damping) * var(--progress))) * 
    var(--amplitude) * 
    cos(var(--frequency) * var(--progress) - var(--offset))
  );

  transform: translateX(var(--oscillation));
  animation: damping 1s linear infinite;
}

如果想做二维的阻尼运动,比如让元素画圈的同时逐渐停下来,可以同时用 sin() 和 cos() 分别控制 X 轴和 Y 轴。

.circle {
  --oscillation-x: calc(
    (exp(-1 * var(--damping) * var(--progress))) * 
    var(--amplitude) * 
    cos(var(--frequency) * var(--progress) - var(--offset))
  );
  --oscillation-y: calc(
    (exp(-1 * var(--damping) * var(--progress))) * 
    var(--amplitude) * 
    sin(var(--frequency) * var(--progress) - var(--offset))
  );
  transform: translateX(var(--oscillation-x)) translateY(var(--oscillation-y));
}

这里面有个容易踩的坑,就是指数函数 exp() 的参数。因为阻尼系数和 –progress 都是正数,所以 exp(-1 * 正数) 的结果会越来越小,这正是阻尼效果需要的。但如果阻尼系数太大,动画可能很快就停了,看不出振荡效果;太小的话又停不下来。所以参数得根据实际效果慢慢调,找到一个合适的中间值。

还有啥三角函数能玩

这次咱们主要盘了 sin() 和 cos() 的用法,其实 CSS 里还有 tan(),以及 asin()、acos()、atan() 和 atan2() 这些反三角函数。它们也能干很多事儿,比如计算复杂曲线的斜率、做更精细的角度控制。等后面有机会再慢慢聊,不过就凭咱们今天聊的这些,已经够给那些“三角函数无用论”啪啪打脸了。