使用新 CSS sin() 和 cos() 三角函数创建时钟

Avatar of Mads Stoumann
Mads Stoumann

DigitalOcean 为您的每个旅程阶段提供云产品。立即开始使用 $200 免费积分!

CSS 三角函数已经出现!当然,如果您使用的是最新版本的 Firefox 和 Safari。在 CSS 中拥有这种数学能力打开了一系列可能性。在本教程中,我想我们会浅尝辄止,感受一下几个较新的函数:sin()cos()

还有其他三角函数正在开发中 - 包括 tan() - 那么为什么只关注 sin()cos()?它们恰好适合我脑海中的想法,即沿着圆的边缘放置文本。当 Chris 分享了一个使用 Sass mixin 的方法 时,这在 CSS-Tricks 上已经讨论过了。那是六年前,所以让我们用最前沿的技术来处理它。

这是我脑海中的想法。再说一次,它目前只在 Firefox 和 Safari 中受支持

所以,它并不完全像单词形成一个圆形,但我们确实沿着圆圈放置了文本字符以形成一个钟面。以下是一些我们可以用来开始的标记

<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-face 容器的一些超级基本样式。我决定使用 <time> 标签,带有 datetime 属性。

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

我在那里装饰了一些东西,但只是为了获得基本形状和背景颜色,帮助我们看到我们在做什么。请注意我们如何在 CSS 变量 中保存 width 值。我们将在稍后使用它。到目前为止,看起来并不多

Large tomato colored circle with a vertical list of numbers 1-12 on the left.

看起来像是某种现代艺术实验,对吧?让我们引入一个新的变量 --_r,用于存储圆的 **半径**,它等于圆宽度的二分之一。这样,如果宽度 (--_w) 发生变化,半径值 (--_r) 也会更新 - 这要归功于另一个 CSS 数学函数 calc()

.clock {
  --_w: 300px;
  --_r: calc(var(--_w) / 2);
  /* rest of styles */
}

现在,做一些数学运算。圆周是 360 度。我们的钟表上有 12 个刻度,所以我们希望每隔 30 度 (360 / 12) 放置一个数字。在数学领域,圆从 3 点钟开始,所以正午实际上比它 **少 90 度**,即 270 度 (360 - 90)。

让我们添加另一个变量 --_d,我们可以使用它来设置钟面上每个数字的 **度数** 值。我们将每隔 30 度递增值以完成我们的圆圈

.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() 函数了!我们要做的是使用它们来获取每个数字的 X 和 Y 坐标,以便我们能够将它们正确地放置在钟面上。

X 坐标的公式为 radius + (radius * cos(degree))。让我们将其插入到我们新的 --_x 变量中

--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));

Y 坐标的公式为 radius + (radius * sin(degree))。我们拥有计算所需的一切

--_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));

我们需要做一些整理工作来设置数字,所以让我们在它们上面添加一些基本样式以确保它们被绝对定位并使用我们的坐标放置

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

请注意 --_sz,我们将在稍后用于数字的 widthheight。让我们看看我们到目前为止做了什么。

Large tomato colored circle with off-centered hour number labels along its edge.

这看起来更像一个钟表了!看到每个数字的左上角是否被正确地定位在圆圈周围了吗?我们需要在计算每个数字的坐标时“缩小”半径。我们可以从圆圈的大小 (--_w) 中 *减去* 数字的大小 (--_sz),然后计算半径

--_r: calc((var(--_w) - var(--_sz)) / 2);
Large tomato colored circle with hour number labels along its rounded edge.

好多了!让我们更改颜色,使其看起来更优雅

A white clock face with numbers against a dark gray background. The clock has no arms.

我们可以就此止步!我们实现了将文本放置在圆圈周围的目标,对吧?但是没有指针显示时、分、秒的钟表有什么意义呢?

让我们为此使用一个 CSS 动画。首先,让我们向我们的标记中添加三个元素,

<div class="clock">
  <!-- after <time>-tags -->
  <span class="arm seconds"></span>
  <span class="arm minutes"></span>
  <span class="arm hours"></span>
  <span class="arm center"></span>
</div>

然后为所有三条指针添加一些通用标记。同样,大多数这些只是确保指针被绝对定位并被相应地放置

.arm {
  background-color: var(--_abg);
  border-radius: calc(var(--_aw) * 2);
  display: block;
  height: var(--_ah);
  left: calc((var(--_w) - var(--_aw)) / 2);
  position: absolute;
  top: calc((var(--_w) / 2) - var(--_ah));
  transform: rotate(0deg);
  transform-origin: bottom;
  width: var(--_aw);
}

我们将对所有三条指针使用 **相同的动画**

@keyframes turn {
  to {
    transform: rotate(1turn);
  }
}

唯一的区别是每条指针完成一圈所用的时间。对于 **时针**,它需要 **12 小时** 才能完成一圈。animation-duration 属性只接受毫秒和秒的单位。让我们坚持使用秒,即 43,200 秒 (60 秒 * 60 分钟 * 12 小时)。

animation: turn 43200s infinite;

**分针** 需要 **1 小时** 才能完成一圈。但我们希望这成为一个 多步动画,以便指针之间的移动是交错而不是线性的。我们需要 60 步,每分钟一步

animation: turn 3600s steps(60, end) infinite;

**秒针** 几乎与 分针 **相同**,但持续时间是 60 秒而不是 60 分钟

animation: turn 60s steps(60, end) infinite;

让我们更新我们在通用样式中创建的属性

.seconds {
  --_abg: hsl(0, 5%, 40%);
  --_ah: 145px;
  --_aw: 2px;
  animation: turn 60s steps(60, end) infinite;
}
.minutes {
  --_abg: #333;
  --_ah: 145px;
  --_aw: 6px;
  animation: turn 3600s steps(60, end) infinite;
}
.hours {
  --_abg: #333;
  --_ah: 110px;
  --_aw: 6px;
  animation: turn 43200s linear infinite;
}

如果我们想从当前时间开始呢?我们需要一些 JavaScript

const time = new Date();
const hour = -3600 * (time.getHours() % 12);
const mins = -60 * time.getMinutes();
app.style.setProperty('--_dm', `${mins}s`);
app.style.setProperty('--_dh', `${(hour+mins)}s`);

我在钟面上添加了 id="app",并在其上设置了两个新的自定义属性,它们设置了一个负数 animation-delay,就像 Mate Marschalko 在 他分享了一个纯 CSS 时钟时所做的那样。JavaScript Date 对象的 getHours() 方法使用的是 24 小时制,因此我们使用 取余运算符 将其转换为 12 小时制。

在 CSS 中,我们也需要添加 animation-delay

.minutes {
  animation-delay: var(--_dm, 0s);
  /* other styles */
}

.hours {
  animation-delay: var(--_dh, 0s);
  /* other styles */
}

**还有一件事。** 使用 CSS @supports 和我们已经创建的属性,我们可以为不支持 sin()cos() 的浏览器提供一个回退。(感谢 Temani Afif!)

@supports not (left: calc(1px * cos(45deg))) {
  time {
    left: 50% !important;
    top: 50% !important;
    transform: translate(-50%,-50%) rotate(var(--_d)) translate(var(--_r)) rotate(calc(-1*var(--_d)))
  }
}

就这样!我们的时钟做好了!这是最终的演示。再说一次,它目前只在 Firefox 和 Safari 中受支持。

我们还能做什么?

只是在这里随便玩玩,但我们可以快速将我们的时钟变成一个圆形图片库,方法是将 <time> 标签替换为 <img>,然后更新宽度 (--_w) 和半径 (--_r) 值

让我们再试一次。我之前提到过时钟看起来有点像现代艺术实验。我们可以利用这一点,重新创作一个我在一家艺术画廊看到的海报(不幸的是我没有买)上的图案。据我记得,它被称为“月亮”,由许多形成一个圆圈的点组成。

A large circle formed out of a bunch of smaller filled circles of various earthtone colors.

这一次我们将使用一个无序列表,因为圆圈没有遵循特定的顺序。我们甚至不会将所有列表项放入标记中。相反,让我们使用 JavaScript 将它们注入,并添加一些我们可以用来操作最终结果的控件。

控件是范围输入 (<input type="range">),我们将它们包装在一个 <form> 中,并监听 input 事件。

<form id="controls">
  <fieldset>
    <label>Number of rings
      <input type="range" min="2" max="12" value="10" id="rings" />
    </label>
    <label>Dots per ring
      <input type="range" min="5" max="12" value="7" id="dots" />
    </label>
    <label>Spread
      <input type="range" min="10" max="40" value="40" id="spread" />
    </label>
  </fieldset>
</form>

我们将对“input”运行此方法,这将创建许多 <li> 元素,并应用我们之前使用的度数 (--_d) 变量到每个元素。我们还可以重新利用我们的半径变量 (--_r)。

我还希望这些点具有不同的颜色。所以,让我们将每个列表项的 HSL 颜色值随机化(好吧,不是 *完全* 随机化),并将其存储为一个新的 CSS 变量 --_bgc

const update = () => {
  let s = "";
  for (let i = 1; i <= rings.valueAsNumber; i++) {
    const r = spread.valueAsNumber * i;
    const theta = coords(dots.valueAsNumber * i);
    for (let j = 0; j < theta.length; j++) {
      s += `<li style="--_d:${theta[j]};--_r:${r}px;--_bgc:hsl(${random(
        50,
        25
      )},${random(90, 50)}%,${random(90, 60)}%)"></li>`;
    }
  }
  app.innerHTML = s;
}

random() 方法将在定义的数字范围内选择一个值

const random = (max, min = 0, f = true) => f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;

就这样。我们使用 JavaScript 来渲染标记,但一旦渲染完成,我们就不再需要它了。sin()cos() 函数帮助我们将所有点定位在正确的位置。

最后的想法

将事物放置在圆圈周围是一个相当基本的示例,用来展示像 sin()cos() 这样的三角函数的功能。但是,我们得到现代 CSS 功能来为旧的解决方法提供新的解决方案,这确实很酷,我相信我们会看到更多有趣、复杂和创造性的用例,尤其是在 Chrome 和 Edge 的浏览器支持到来时。