制作一个能显示当前时间的纯CSS时钟的方法

763 阅读17分钟

让我们用CSS自定义属性calc() 功能建立一个功能齐全、可设置的 "模拟 "时钟。然后我们再把它转换成一个 "数字 "时钟。所有这些都不需要使用JavaScript!

下面是我们将制作的时钟的简要介绍。

刷新calc() 函数的内容

CSS预处理器以其计算CSS数值的能力永远逗弄着我们。预处理器的问题是,它们缺乏对CSS代码编译后的上下文的了解。这就是为什么你不可能说你希望你的元素宽度是容器的100%减去50像素。这在预处理程序中会产生一个错误。

width: 100% - 50px;
// error: Incompatible units: 'px' and '%'

预处理器,顾名思义,对你的指令进行预处理,但它们的输出仍然只是普通的CSS,这就是为什么它们不能调和你的算术运算中的不同单位。Ana已经很详细地介绍了Sass和CSS特性之间的冲突

好消息是,原生的CSS计算不仅是可能的,而且我们甚至可以通过calc() 函数结合不同的单位,如像素和百分比。

width: calc(100% - 50px);

calc() 可以在任何允许使用长度、频率、角度、时间、百分比、数字或整数的地方使用。请查看《CSS指南》中关于CSS函数的完整概述。

使之更加强大的是,你可以将calc() 与CSS自定义属性结合起来--这是预处理器所不能处理的。

钟的指针

Image of a round clock with a light grey background. The hours hand is dark blue pointed at 2, the minutes are light blue pointed at 9 and the seconds are yellow and pointed at 2.

让我们先用几个自定义属性和模拟时钟的指针的动画定义来打好基础。

:root {
  --second: 1s;
  --minute: calc(var(--second) * 60);
  --hour: calc(var(--minute) * 60);
}
@keyframes rotate {
  from { transform: rotate(0); }
  to { transform: rotate(1turn); }
}

一切都从根上下文中的--second 自定义属性开始,在这里我们定义了一秒钟应该是,嗯,一秒钟(1s )。所有未来的值和时间都将从这里衍生出来。

这个属性本质上是我们时钟的核心,它控制着时钟的所有指针的快慢。将--second 设置为1s ,使时钟与现实生活中的时间一致,但我们可以通过将其设置为2s ,使其以半速前进,甚至通过设置为10ms ,使其以100倍的速度前进。

我们要计算的第一个属性是--minute 指针,我们希望它等于一秒钟的60倍。我们可以参考--second 属性的值,并在calc() 的帮助下将其乘以60。

--minute: calc(var(--second) * 60);

--hour 指针属性的定义原理完全相同,但要乘以--minute 指针值。

--hour: calc(var(--minute) * 60);

我们希望时钟上的所有三根指针都能从0到360度旋转--围绕着钟面的形状旋转!这就是我们的目标。这三个动画的区别在于每个动画需要多长时间才能全部转完。我们可以使用完全有效的CSS值1turn ,而不是使用360deg ,作为我们的全圆值。

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

这些@keyframes ,只是告诉浏览器在动画过程中让元素转一圈。我们已经定义了一个名为rotate 的动画,现在可以把它分配给时钟的秒针。

.second.hand {
  animation: rotate steps(60) var(--minute) infinite;
}

我们正在使用 [animation](https://css-tricks.com/almanac/properties/a/animation/)简称属性来定义动画的细节。我们添加了动画的名称(rotate ),我们希望动画运行多长时间(var(--minute) ,即60秒),以及运行多少次(infinite ,即永不停止运行)。steps(60) 是动画计时函数,它告诉浏览器以60个相同的步骤执行1圈动画。这样一来,秒针在每一秒钟都会跳动,而不是沿着圆周平滑地旋转。

在我们讨论CSS动画的时候,如果我们想让动画晚一点开始,我们可以定义一个动画延迟(animation-delay),我们还可以用animation-direction 来改变动画是向前还是向后播放。我们甚至可以用animation-play-state 暂停和重新启动动画。

分针和时针的动画效果与秒针的非常相似。不同的是,这里不需要多个步骤--这些指针可以以平滑、线性的方式旋转。

分针需要一个小时来完成一个完整的旋转,所以。

.minute.hand {
  animation: rotate linear var(--hour) infinite;
}

另一方面(双关语),时针需要12个小时才能转一圈。我们没有一个单独的自定义属性来表示这个时间量,比如--half-day ,所以我们将--hour 乘以12。

.hour.hand {
  animation: rotate linear calc(var(--hour) * 12) infinite;
}

你现在可能已经明白了时钟的指针是如何工作的了。但如果我们不实际建造时钟,就不是一个完整的例子。

钟面

到目前为止,我们只看了时钟的CSS方面。我们还需要一些HTML来实现所有这些工作。这是我正在使用的东西。

<main>
  <div class="clock">
    <div class="second hand"></div>
    <div class="minute hand"></div>
    <div class="hour hand"></div>
  </div>
</main>

让我们来看看我们要做什么来为我们的时钟设置样式。

.clock {
  width: 300px;
  height: 300px;
  border-radius: 50%;
  background-color: var(--grey);
  margin: 0 auto;
  position: relative;
}

我们使时钟的高度和宽度为300px,使背景颜色为灰色(使用一个自定义属性--grey ,我们可以稍后定义),并将其变成一个边界半径为50%的圆形。

钟上有三个指针。让我们首先用绝对定位把这些移到钟面的中心。

.hand {
  position: absolute;
  left: 50%;
  top: 50%;
}

注意这个类的名字(.hands),因为所有三根指针都使用它作为它们的基本样式。这一点很重要,因为对这个类的任何改变都会应用到所有三个指针。

让我们来定义手的尺寸并给它上色。

.hand {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 150px;
  background-color: var(--blue);
}

手现在都到位了。

A round clock with just one light blue hand pointing directly at 6.

获得适当的旋转

让我们先别急着庆祝。我们有几个问题,当钟的指针这么细时,它们可能并不明显。如果我们把它们改成100px宽,会怎么样?

A round clock with a light grey background. The light blue hand is thick and still pointed at 6, but

我们现在可以看到,如果一个元素从左边定位50%,它就会对准父元素的中心,但这并不是我们所需要的。为了解决这个问题,左边的坐标需要是50%,减去手的宽度的一半,在我们的例子中是50px。

Grey circle with a red vertical line bisecting it in the center. Two red arrows are on the clock, one labeled 50% pointing at the red line and another labeled negative 50 pixels pointing away from the red line.

对于calc() 功能来说,处理多个不同的测量值是很容易的。

.hand {
  position: absolute;
  left: calc(50% - 50px);
  top: 50%;
  width: 100px;
  height: 150px;
  background-color: var(--grey);
}

这就解决了我们的初始定位问题,然而,如果我们试图旋转元素,我们可以看到变换原点,也就是旋转的支点,是在元素的中心。

Grey circle showing a blue rectangle on top rotated counter-clockwise.

我们可以使用 [transform-origin](https://css-tricks.com/almanac/properties/t/transform-origin/)属性来改变旋转原点,使其位于X轴的中心和Y轴的顶端。

.hand {
  position: absolute;
  left: calc(50% - 50px);
  top: 50%;
  width: 100px;
  height: 150px;
  background-color: var(--grey);
  transform-origin: center 0;
}

这很好,但并不完美,因为我们的时钟指针的像素值是硬编码的。如果我们想让我们的指针有不同的宽度和高度,并随着时钟的实际大小而变化呢?是的,你猜对了:我们需要一些CSS的自定义属性

.second {
  --width: 5px;
  --height: 140px;
  --color: var(--yellow);
}
.minute {
  --width: 10px;
  --height: 90px;
  --color: var(--blue);
}
.hour {
  --width: 10px;
  --height: 50px;
  --color: var(--dark-blue);
}

有了这个,我们就为各个指针定义了自定义属性。这里有趣的是,我们给这些属性起了相同的名字。--width,--height, 和--color 。我们给了它们不同的值,但它们没有互相覆盖,这怎么可能呢?如果我调用var(--width),var(--height)var(--color) ,我将得到哪个值呢?

让我们来看看时针的情况。

<div class="hour hand"></div>

我们给.hour 类分配了新的自定义属性,它们在元素上有局部范围,包括该元素和它的所有子元素。这意味着任何应用于该元素或其子元素的CSS样式在访问自定义属性时都会看到并使用在其自身范围内设置的特定值。因此,如果你在时针元素或其内部的任何祖先元素中调用var(--width) ,从我们上面的例子中返回的值是10px。这也意味着,如果我们试图在这些元素之外使用任何这些属性--例如在body元素内--它们就不能被范围之外的元素所访问。

接着是秒和分的指针,我们进入一个不同的范围。这意味着自定义属性可以用不同的值重新定义。

.second {
  --width: 5px;
  --height: 140px;
  --color: var(--yellow);
}
.minute {
  --width: 10px;
  --height: 90px;
  --color: var(--blue);
}
.hour {
  --width: 10px;
  --height: 50px;
  --color: var(--dark-blue);
}
.hand {
  position: absolute;
  top: 50%;
  left: calc(50% - var(--width) / 2);
  width: var(--width);
  height: var(--height);
  background-color: var(--color);
  transform-origin: center 0;
}

这样做的好处是,.hand 类(我们分配给所有三个指针元素)可以引用这些属性进行计算和声明。而每只手都会从自己的范围内收到自己的属性。在某种程度上,我们为每个元素个性化了.hand 类,以避免代码中不必要的重复。

我们的时钟已经启动并运行,所有的指针都在以正确的速度移动。

The finished clock. Grey background, with minutes and hours pointing to 6 and seconds pointing to 7.

我们可以到此为止,但让我提出一些改进建议。钟上的指针从6点钟方向开始,但我们可以通过将时钟旋转180度将其初始位置设置为12点钟方向。让我们把这一行添加到.clock 类中。

.clock {
  /* same as before */
  transform: rotate(180deg);
}

指针如果有圆形的边缘可能会很好看。

.hand {
  /* same as before */
  border-radius: calc(var(--width) / 2);
}

我们的时钟看起来和工作起来都很好!而且所有的指针都是从12点开始的,正是我们想要的样子

设置时钟

即使有了所有这些了不起的功能,这个时钟也是无法使用的,因为它在显示实际时间方面失败得很厉害。然而,我们在这方面有一些硬性限制。我们根本不可能用HTML和CSS访问当地时间来自动设置我们的时钟。但我们可以为手动设置做准备。

我们可以将时钟设置为从某个小时和分钟开始,如果我们正好在那个时间运行HTML,它就会在之后准确地保持时间。这基本上就是你在现实世界中设置时钟的方式,所以我认为这是一个可以接受的解决方案。😅

让我们把我们想要启动时钟的时间作为自定义属性添加到.clock 类里面。

.clock {
  --setTimeHour: 16;
  --setTimeMinute: 20;
  /* same as before */
}

在我写作的时候,我的当前时间即将达到16:20(或4:20),所以时钟将被设置为这个时间。我所要做的就是在16:20刷新页面,它将准确地保持时间。

好的,但是......如果一个CSS动画已经控制了旋转,我们怎么能把时间设置到这些位置并旋转指针?

理想情况下,我们想在动画开始的时候,将时钟的指针旋转并设置到一个特定的位置。比如你想把时针设置成90度,这样它就从下午3点开始,并从这个位置初始化旋转动画。

/* this will not work */
.hour.hand {
  transform: rotate(90deg);
  animation: rotate linear var(--hour) infinite;
}

那么,不幸的是,这将不会起作用,因为transform ,立即被动画覆盖,因为它修改的正是transform 属性。所以,不管我们把它设置成什么,它都会回到动画的第一个关键帧开始的0度。

我们可以独立于动画来设置手的旋转。例如,我们可以将手包入一个额外的元素。这个额外的父元素,即 "setter",将负责设置初始位置,然后里面的手元素可以独立地从0到360度进行动画。开始的0度位置将是相对于我们设置的父元素setter的位置。

这将是可行的,但幸运的是,有一个更好的选择!让我给你一个惊喜吧**让我给你一个惊喜吧!**🪄✨✨

animation-delay 属性是我们通常用来启动动画的一些预定义的延迟。

诀窍是**使用一个负值,**它可以立即启动动画,但是从动画时间轴上的一个特定点开始!

要在动画的10秒后开始。

animation-delay: -10s;

对于时针和分针来说,我们需要的实际延迟值是由--setTimeHour--setTimeMinute 属性计算出来的。为了帮助计算,让我们创建两个新的属性,其中包含我们需要的时间转移量。

  • 时针需要是我们想要设置时钟的小时,乘以一个小时。
  • 分针的移动量是我们想要设置的时钟的分钟数乘以一分钟。
--setTimeHour: 16;
--setTimeMinute: 20;
--timeShiftHour: calc(var(--setTimeHour) * var(--hour));
--timeShiftMinute: calc(var(--setTimeMinute) * var(--minute));

这些新的属性包含了我们需要的确切时间量,animation-delay 属性来设置我们的时钟。让我们把这些添加到我们的动画定义中。

.second.hand {
  animation: rotate steps(60) var(--minute) infinite;
}
.minute.hand {
  animation: rotate linear var(--hour) infinite;
  animation-delay: calc(var(--timeShiftMinute) * -1);
}
.hour.hand {
  animation: rotate linear calc(var(--hour) * 12) infinite;
  animation-delay: calc(var(--timeShiftHour) * -1);
}

注意我们是如何将这些值乘以-1来转换为一个负数的。

我们这样做几乎达到了完美的效果,但是有一个小问题:比如说,如果我们把分钟数设置为30,那么时针需要已经到了下一个小时的一半。更糟糕的情况是,将分钟数设置为59,而时针仍在一小时的开始位置。

在这种情况下,我们所要做的就是将分针和时针的移位值加在一起。

.hour.hand {
  animation: rotate linear calc(var(--hour) * 12) infinite;
  animation-delay: calc(
    (var(--timeShiftHour) + var(--timeShiftMinute)) * -1
  );
}

我认为这样我们就解决了一切问题。让我们欣赏一下我们美丽的、纯CSS的、可设置的、模拟的时钟。

我们来看看数字时钟

原则上,模拟时钟和数字时钟都使用相同的计算方法,区别在于计算的视觉表现。

Clock with a rectangular light grey background with the numeric time reading 14 45 18 in 24-hour format.

我的想法是:我们可以通过设置高的、垂直的数字列来创建一个数字时钟,并对这些数字进行动画处理,而不是旋转时钟指针。从最终版本的时钟容器中移除溢出掩码,就能发现其中的诀窍。

新的HTML标记需要包含时钟所有三个部分的所有数字,从秒和分钟部分的00到59,小时部分的00到23。

<main>
  <div class="clock">
    <div class="hour section">
      <ul>
        <li>00</li>
        <li>01</li>
        <!-- etc. -->
        <li>22</li>
        <li>23</li>
      </ul>
    </div>
    <div class="minute section">
      <ul>
        <li>00</li>
        <li>01</li>
        <!-- etc. -->
        <li>58</li>
        <li>59</li>
      </ul>
    </div>
    <div class="second section">
      <ul>
        <li>00</li>
        <li>01</li>
        <!-- etc. -->
        <li>58</li>
        <li>59</li>
      </ul>
    </div>
  </div>
 </main>

为了使这些数字保持一致,我们需要编写一些CSS,但为了开始设计样式,我们可以直接从模拟时钟的:root 范围内复制自定义属性,因为时间是通用的。

:root {
  --second: 1s;
  --minute: calc(var(--second) * 60);
  --hour: calc(var(--minute) * 60);
}

最外层的包装元素,.clock ,仍然有相同的自定义属性来设置初始时间。所改变的只是它变成了一个flexbox容器。

.clock {
  --setTimeHour: 14;
  --setTimeMinute: 01;
  --timeShiftHour: calc(var(--setTimeHour) * var(--hour));
  --timeShiftMinute: calc(var(--setTimeMinute) * var(--minute));

  width: 150px;
  height: 50px;
  background-color: var(--grey);
  margin: 0 auto;
  position: relative;
  display: flex;
}

三个无序列表和其中容纳数字的列表项不需要任何特殊处理。如果数字的水平空间有限,它们就会相互堆叠在一起。我们只需确保没有列表样式,以防止弹出点,并确保我们将东西放在中心位置,以保持一致。

.section > ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.section > ul > li {
  width: 50px;
  height: 50px;
  font-size: 32px;
  text-align: center;
  padding-top: 2px;
}

布局已经完成了!

在每个无序列表的外面是一个<div> ,有一个.section 类。这是包裹每个部分的元素,所以我们可以用它作为一个掩码来隐藏时钟可见区域之外的数字。

.section {
  position: relative;
  width: calc(100% / 3);
  overflow: hidden;
}

时钟的结构已经完成,现在准备对轨道进行动画处理。

给数字钟做动画

整个动画背后的基本想法是,三个数字条可以在它们的遮罩内上下移动,以显示不同的数字,从0到59代表秒和分钟,0到23代表小时(24小时制)。

我们可以通过改变CSS中各个数字条的translateY 过渡功能,从0到-100%来做到这一点。这是因为Y轴上的100%代表了整个条形图的高度。0%的值将显示第一个数字,而100%将显示当前条带的最后一个数字。

以前,我们的动画是基于将一只手从0到360度的旋转。现在,我们有一个不同的动画,在Y轴上将数字条从0移动到-100%。

@keyframes tick {
  from { transform: translateY(0); }
  to { transform: translateY(-100%); }
}

将这个动画应用到秒数条上,可以用与模拟时钟相同的方式进行。唯一的区别是选择器和被引用的动画的名称。

.second > ul {
  animation: tick steps(60) var(--minute) infinite;
}

step(60) 的设置下,动画在数字之间跳动,就像我们在模拟钟上的秒针那样。我们可以把它改为ease ,然后数字就会平滑地上下滑动,就像它们在纸带上一样。

将新的滴答动画分配给分钟和小时部分也同样简单明了。

.minute > ul {
  animation: tick steps(60) var(--hour) infinite;
  animation-delay: calc(var(--timeShiftMinute) * -1);
}
.hour > ul {
  animation: tick steps(24) calc(24 * var(--hour)) infinite;
  animation-delay: calc(var(--timeShiftHour) * -1);
}

同样,声明非常相似,这次不同的是选择器、计时功能和动画名称。

现在时钟滴答作响并保持正确的时间。

Time reading 14 1 10 in 24-hour format.

还有一个细节。闪烁的冒号 (:)

我们也可以在这里停下来,然后结束今天的工作。但我们还可以做最后一件事,使我们的数字时钟更加真实:使分和秒之间的冒号分隔符在每一秒钟过去时闪烁。

我们可以在HTML中添加这些冒号,但它们不是内容的一部分。我们希望它们是对时钟外观和风格的一种增强,所以CSS是存储这些内容的正确位置。这就是content 属性的作用,我们可以在::after 伪元素上使用它来表示分钟和小时。

.minute::after,
.hour::after {
  content: ":";
  margin-left: 2px;
  position: absolute;
  top: 6px;
  left: 44px;
  font-size: 24px;
}

这很容易但我们怎样才能使秒的冒号也闪烁呢?我们想让它变成动画,所以我们需要定义一个新的动画?有很多方法可以实现这一点,但我想这次我们应该改变content 属性,以证明,很意外地,在动画中改变其值是可能的。

@keyframes blink {
  from { content: ":"; }
  to { content: ""; }
}

content 属性做动画并不是在每个浏览器中都能实现的,所以你可以把它改为opacityvisibility ,作为一个安全的选择...

最后一步是将闪烁动画分配给分钟部分的伪元素。

.minute::after {
  animation: blink var(--second) infinite;
}

**就这样,我们全部完成了!**数字时钟准确地保持着时间,我们甚至还设法在数字之间添加了一个闪烁的分隔符。