CSS三角函数里的反函数家族,到底怎么用才能解决实际问题?

2,098字
9–13 分钟
in

在CSS里折腾三角函数,以前总觉得是数学课代表才干的活。但真上手之后发现,这玩意儿简直是布局界的“瑞士军刀”。尤其是当碰到需要根据元素尺寸自动计算角度,或者让某个元素跟着鼠标视线走这种需求时,atan()atan2() 这俩反函数,简直就是“神队友”。

目录

反函数是啥

在聊具体用法之前,得先整明白这俩兄弟是干啥的。咱们都知道 sin()cos()tan(),给它们一个角度,它们能算出一个比例值。而 atan()atan2() 正好是反着来的:给一个比例值,它们能反推回角度。这就好比知道了股价涨跌幅度,反推出大盘涨了多少个点,逻辑上差不多是互逆的操作。

不过 asin()acos() 这俩限制比较多,它们只认 -11 之间的数,超出范围就直接 NaN 了,有点“挑食”。而 atan() 就豪放多了,整个数轴上的数它都接得住,输入从负无穷到正无穷,输出角度在 -90deg90deg 之间。所以日常实战里,atan() 的出镜率明显高得多。

用atan()给渐变打工人安排明白

很多同学可能都遇到过这种尴尬:给 conic-gradient() 写死了角度,结果页面一缩放,渐变的位置就跟原本的设计图对不上了。比如写了个 from 45deg,宽高比例一变,原本该在角落的颜色块就飘走了。

其实解决这个问题,核心就是算出元素宽高比例对应的那个角度。把元素的宽度当成三角形的邻边,高度当成对边,要算的就是那个夹角。

.element {
  /* 获取宽高比例对应的角度 */
  --angle: atan(var(--height) / var(--width));
  /* conic-gradient默认从顶部开始,需要旋转一下 */
  --rotation: calc(90deg - var(--angle));
  background: conic-gradient(
    from var(--rotation),
    #84a59d 180deg,
    #f28482 180deg
  );
}

这里有个地方容易踩坑:atan() 算出来的角度是基于邻边和对边的比例,但 conic-gradientfrom 方向是从顶部(也就是 0deg)开始顺时针转。所以必须用 90deg 减去算出来的角度,才能让渐变起点对准对角。要是直接拿算好的角度硬怼,颜色块位置会直接跑偏,别问我怎么知道的,说多了都是泪。

跟鼠标视线联动,atan2()才是YYDS

想让页面上的某个元素一直盯着鼠标转,这个需求听着就很带感。比如做个吉祥物,眼睛跟着鼠标走,这种效果搁以前得写一堆JS监听 mousemove 再算角度,现在用 atan2() 配合CSS变量,代码清爽多了。

先用JS把鼠标坐标喂给CSS:

const body = document.querySelector("body");

body.addEventListener("pointermove", (event) => {
  let x = event.clientX;
  let y = event.clientY;
  body.style.setProperty("--m-x", `${Math.round(x)}px`);
  body.style.setProperty("--m-y", `${Math.round(y)}px`);
});

接下来就是见证奇迹的时刻。假设眼睛元素本身已经有个初始角度(比如朝右看),那就直接在 rotate 里加上 atan2() 算出的角度。

.eye {
  rotate: calc(135deg + atan2(var(--m-y), var(--m-x)));
}

为啥要用 atan2() 而不是 atan()?这里就是区别所在了。atan(y/x) 在计算时,如果x和y都是负数,它分不清是第二象限还是第四象限,算出来的角度会搞混。而 atan2(y, x) 这货聪明就聪明在,它同时看x和y的正负号,能精确返回从正x轴算起的正确角度,横跨四个象限完全不带含糊的。如果这时候用 atan() 硬算,当鼠标移动到左上方区域,眼睛的转向会突然抽风,直接原地爆炸。

坐标原点也是个细节问题。如果直接拿 --m-x--m-y 来算,坐标原点在视口左上角。但眼睛元素可能距离左上角有段距离,这时候需要把偏移量减掉,坐标系移到眼睛元素身上,这样转向才准。

额外彩蛋:用atan2()把视口宽度撸成数字

这招是Jane Ori大佬的骚操作,用 atan2()tan()100vw 这种带单位的长度值转成纯整数。平时想用视口宽度做数学计算,最烦的就是单位没法直接参与运算,这招完美破局。

@property --100vw {
  syntax: "<length>";
  initial-value: 0px;
  inherits: false;
}

:root {
  --100vw: 100vw;
  --int-width: calc(10000 * tan(atan2(var(--100vw), 10000px)));
}

原理其实挺巧妙:atan2(100vw, 10000px) 会算出 100vw 相对于 10000px 的比例对应的弧度,再用 tan() 把弧度转回比例值,最后乘上 10000 得到整数值。这么一套组合拳下来,--int-width 就是不带单位、干干净净的视口宽度数值了。