CSS三角函数sin()和cos()来了,怎么把文字围成圆圈排布?

5,563字
24–35 分钟
in

搞前端的小伙伴应该都听说了,CSS最近整了个大活,直接塞进去一套三角函数。Firefox和Safari的最新版已经能用上sin()cos(),Chrome和Edge还在赶进度。这俩函数能干啥?举个栗子,想把一串文字或者数字沿着圆形边缘排布,以前得靠JS或者Sass循环硬算,现在几行CSS就搞定。咱们今天就拿它来手搓一个表盘,顺便再整两个骚操作——圆形图片墙和动态点阵,小白也能跟着搞定。

目录

啥是sin()和cos()

三角函数在数学里就是用来算直角三角形边角关系的。放在CSS里,sin()cos()接收一个角度值(比如30deg1rad0.25turn),然后返回一个介于-1到1之间的数。拿圆来说,圆心角对应的X坐标可以用半径 + 半径 * cos(角度),Y坐标用半径 + 半径 * sin(角度)。这样就能算出圆上任何一个点的精确位置,不用再瞎蒙偏移量。比如角度0degcos(0deg)=1sin(0deg)=0,对应的就是圆的最右侧中点。

手搓一个表盘

先搭好HTML结构,一个容器包着12个数字标签。

<div class="clock">
  <div class="clock-face">
    <time datetime="12:00">12</time>
    <time datetime="1:00">1</time>
    <time datetime="2:00">2</time>
    <time datetime="3:00">3</time>
    <time datetime="4:00">4</time>
    <time datetime="5:00">5</time>
    <time datetime="6:00">6</time>
    <time datetime="7:00">7</time>
    <time datetime="8:00">8</time>
    <time datetime="9:00">9</time>
    <time datetime="10:00">10</time>
    <time datetime="11:00">11</time>
  </div>
</div>

画个圆底盘

.clock设为正方形,用border-radius: 50%捏成圆形。宽度用clamp()做响应式,最小5rem,最大40rem。再加个container-type: inline方便后续用cqi单位。

.clock {
  --_ow: clamp(5rem, 60vw, 40rem);
  width: var(--_ow);
  height: var(--_ow);
  background-color: tomato;
  border-radius: 50%;
  container-type: inline;
  display: grid;
  place-content: center;
  position: relative;
}

这时候只看到一个红色圆饼,数字还没定位。圆饼的宽度存成变量--_ow,后面半径要靠它。

给每个数字分配角度

一圈360°,12个数字每隔30°站一个。但数学上圆的0°在三点钟方向,十二点实际上是-90°(也就是270°)。写CSS变量挨个赋值:

.clock time:nth-child(1) { --_d: 270deg; }
.clock time:nth-child(2) { --_d: 300deg; }
.clock time:nth-child(3) { --_d: 330deg; }
.clock time:nth-child(4) { --_d: 0deg; }
.clock time:nth-child(5) { --_d: 30deg; }
.clock time:nth-child(6) { --_d: 60deg; }
.clock time:nth-child(7) { --_d: 90deg; }
.clock time:nth-child(8) { --_d: 120deg; }
.clock time:nth-child(9) { --_d: 150deg; }
.clock time:nth-child(10) { --_d: 180deg; }
.clock time:nth-child(11) { --_d: 210deg; }
.clock time:nth-child(12) { --_d: 240deg; }

用sin()和cos()算坐标

半径等于宽度的一半:--_r: calc(var(--_ow) / 2)。每个数字的X偏移 = 半径 + 半径 × cos(角度),Y偏移 = 半径 + 半径 × sin(角度)。这里要注意,数字本身有宽高,直接按圆边缘定位会把数字的左上角怼到圆边上,看着就像数字往外跑。解决方法是算半径时先把数字的尺寸减掉:--_sz: 12cqi; 然后--_r: calc((var(--_ow) - var(--_sz)) / 2)。这样数字的中心点才会落在圆周上。

.clock-face time {
  --_sz: 12cqi;
  --_r: calc((var(--_ow) - var(--_sz)) / 2);
  --_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
  --_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));
  width: var(--_sz);
  height: var(--_sz);
  position: absolute;
  left: var(--_x);
  top: var(--_y);
  display: grid;
  place-content: center;
}

写完之后刷新,数字已经整整齐齐围成一圈。如果发现位置偏了,多半是--_sz没减到位,或者cos()/sin()的单位不是deg——记得在变量里带上deg后缀。

加上表针

表针需要三根:时针、分针、秒针。在.clock-face里加三个span,分别给类名hoursminutesseconds,再加一个中心圆点。

<span class="arm seconds"></span>
<span class="arm minutes"></span>
<span class="arm hours"></span>
<span class="arm center"></span>

公共样式让所有指针绝对定位,底边对齐圆心:

.arm {
  position: absolute;
  bottom: 50%;
  left: 50%;
  transform-origin: bottom;
  transform: translateX(-50%) rotate(0deg);
  width: 6px;
  border-radius: 6px;
  background-color: #333;
}
.seconds { height: 145px; width: 2px; background-color: hsl(0, 5%, 40%); }
.minutes { height: 145px; }
.hours { height: 110px; }
.center { width: 16px; height: 16px; background-color: #333; border-radius: 50%; bottom: calc(50% - 8px); left: calc(50% - 8px); transform: none; }

旋转动画用@keyframes turn { to { transform: translateX(-50%) rotate(1turn); } }。分针和秒针要一格一格跳,用steps(60, end);时针平滑转动,用linear

.seconds { animation: turn 60s steps(60, end) infinite; }
.minutes { animation: turn 3600s steps(60, end) infinite; }
.hours { animation: turn 43200s linear infinite; }

想让时钟显示真实时间,需要加一丢丢JS,设置animation-delay为负值,偏移到当前时刻。获取当前小时和分钟,转成秒数,然后塞给CSS变量。

const now = new Date();
const hour = now.getHours() % 12;
const minute = now.getMinutes();
const second = now.getSeconds();
const hourDeg = (hour * 3600 + minute * 60 + second) * -1;
const minuteDeg = (minute * 60 + second) * -1;
document.querySelector('.clock').style.setProperty('--_dh', `${hourDeg}s`);
document.querySelector('.clock').style.setProperty('--_dm', `${minuteDeg}s`);

然后在.hours.minutes里加上animation-delay: var(--_dm, 0s);。刷新,指针直接跳到当前时间。如果表针位置不对,检查一下transform-origin是不是bottom,以及bottom: 50%是否真的让底边对齐了圆心。

整点活,圆形图片墙

把上面表盘里的<time>标签全换成<img>,其他CSS基本不用动。图片的宽高用--_sz控制,object-fit: cover防止变形。为了让图片更紧凑,可以把半径调小一点,比如--_r: calc((var(--_ow) - var(--_sz) * 2) / 2)。一个圆形相册瞬间搞定,鼠标悬停还能加放大特效。这个玩法的核心还是sin()cos()算坐标,换汤不换药。

还能更骚,动态点阵

之前那个表盘看着像现代艺术?那干脆放飞自我,整一个动态点阵。用<ul><li>,每个小圆点都靠sin()/cos()定位。点太多不想手写HTML,用JS动态生成,但定位的脏活累活全甩给CSS。

<form id="controls">
  <label>圈数 <input type="range" min="1" max="12" value="5" id="rings"></label>
  <label>每圈点数 <input type="range" min="4" max="20" value="8" id="dots"></label>
  <label>散开程度 <input type="range" min="10" max="60" value="30" id="spread"></label>
</form>
<ul class="dot-ring"></ul>

JS里监听input事件,循环生成<li>,每个li的内联样式塞两个变量--_d(角度)和--_r(半径)。半径随圈数递增:--_r: calc(圈数 * 散开基数)px。角度等分360°,每圈的点数可以不一样,甚至每圈递增点数,做出螺旋效果。

const rings = document.getElementById('rings');
const dots = document.getElementById('dots');
const spread = document.getElementById('spread');
const container = document.querySelector('.dot-ring');

function generate() {
  let html = '';
  const ringsVal = parseInt(rings.value);
  const dotsPerRing = parseInt(dots.value);
  const spreadVal = parseInt(spread.value);
  for (let r = 1; r <= ringsVal; r++) {
    const radius = r * spreadVal;
    const count = dotsPerRing * r;  // 每圈点数递增
    for (let i = 0; i < count; i++) {
      const angle = (i / count) * 360;
      html += `<li style="--_d: ${angle}deg; --_r: ${radius}px; --_bgc: hsl(${Math.random() * 360}, 70%, 60%);"></li>`;
    }
  }
  container.innerHTML = html;
}
rings.addEventListener('input', generate);
dots.addEventListener('input', generate);
spread.addEventListener('input', generate);
generate();

CSS里每个liposition: absoluteleft: calc(50% + var(--_r) * cos(var(--_d)))top同理。注意lefttop要从圆心开始偏移,所以先left: 50%再通过transform: translate(-50%, -50%)让圆点自身中心对齐,然后加上偏移量。直接撸:

.dot-ring li {
  position: absolute;
  left: calc(50% + var(--_r) * cos(var(--_d)));
  top: calc(50% + var(--_r) * sin(var(--_d)));
  transform: translate(-50%, -50%);
  width: 12px;
  height: 12px;
  background-color: var(--_bgc);
  border-radius: 50%;
}

拖动滑块,圈数和点数实时变化,每个小圆点像被磁铁吸住一样重新排列。这个例子里sin()cos()仍然是大功臣,没有它们,光靠transform: rotatetranslate会绕晕。