纯CSS 使用原生三角函数完成时钟效果

716 阅读2分钟

起因

最近朋友问我是否可以使用纯CSS来实现图中的一个时钟效果,本来我是拒绝的因为这看起来不太可能用纯CSS来实现,因为这种圆盘形状的布局在常规CSS中并没有办法实现,直到...

01.png

直到我发现了CSS最新支持的数据计算!

CSS:层叠样式表 sin() 我们可以看MDN上对此的详细解释和定义

CSS 函数 sin() 为三角函数,返回某数的正弦值,此值介于 -1 和 1 之间。此函数含有单个计算式,此式须将参数结果按弧度数解析为 或 ,即 sin(45deg)、sin(0.125turn) 和 sin(3.14159 / 4) 均表示同一值,约为 0.707。

简单点说我们可以直接在CSS中使用三角函数举个栗子!

如上图我们可以看到数字表盘是从1~12围绕中心点排列而成,这在以前来说除非使用一些奇淫巧计是无法完成的。如今只需要根据三角函数定义就可以轻松完成!

可以看到在上面提供的例子中我们设置了几个CSS变量

  • --size 容器大小
  • --c 中心点坐标
  • --r 半径长度

此时我们复习一下之前在H5 实现超丝滑ChatGPT语音交互中使用过的圆的极坐标公式

已知圆的半径 𝑟 和圆心坐标 (𝑥0,𝑦0),要求出圆周上 θ 的点的坐标。可以使用如下公式计算:

(x,y)=(x0 + r⋅cos(θ),y0 + r⋅sin(θ))


.wrapper {
  @for $i from 1 through 12 {
    div:nth-child(#{$i}) {
      --d: #{30deg * ($i - 1)};
    }
  }
}

.wrapper > div {
  position: absolute;
  --x: calc(var(--c) + (var(--r) * cos(var(--d))));
  --y: calc(var(--c) + (var(--r) * sin(var(--d))));
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%);
}

带入公式我们就很好理解了,首先在div中声明每个数值的角度,这里我们使用了scss来方便批量重复生成数据,一个圆是360°而我们有12个数字那自然每个数字间隔就是360° / 12 = 30°,确定了角度后我们又有中心点半径自然可以得到具体的坐标信息,这里要注意的一点是算出来的xy是基于左上角来布局的

02.png

所以我们需要加上transform: translate(-50%, -50%)来让时间居中显示!

刻度!

掌握了上面的布局技巧后我们就可以顺利成章的继续往下处理时钟所需要的刻度时钟分钟秒钟

我们已经完成了表盘时间,接下来我们依葫芦画瓢把表盘的刻度也给实现,观察设计稿我们能知道一共有60个刻度360° / 60 = 6°

此时要注意的是我们的刻度需要一个旋转角度,旋转角度其实就是的不断递增,代码如下:


.clock-face > li {
  display: block;
  width: 2px;
  height: 4px;
  background: #929394;
  --_x: calc(var(--c) + (var(--face_r) * cos(var(--d))));
  --_y: calc(var(--c) + (var(--face_r) * sin(var(--d))));
  top: calc(var(--_y));
  left: calc(var(--_x));
  position: absolute;
  font-size: 12px;
  opacity: .3;
}

.clock-face > li {

  @for $i from 1 through 60 {
    &:nth-child(#{$i}) {
      --d: #{6deg * ($i - 1)};
      transform: translate(-50%, -50%) rotate(90deg + ($i - 1) * 6deg);
    }
  }

}

可以看到使用sincos能相当轻松完成这种圆形围绕的布局结构!

时针/分针/秒针

这一部分那就更简单啦~只需要使用常规布局手段即可。

.hand {
  --size: 134px;
  position: absolute;
  background: #0A0A0A;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  width: var(--size);
  height: var(--size);
  z-index: 9;

  &:after{
    content: '';
    display: block;
    position: absolute;
    top: 50%;
    left: 50%;
    width: 14px;
    height: 14px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(circle at center, #0A0A0A 40%, #FFAC0B 42%);
  }

  .second {
    width: 4px;
    height: 84px;
    background: #FFAC0B;
    position: absolute;
    left: 50%;
    top: -18px;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
    transform-origin: bottom;
    transform: translate(-50%) rotate(45deg);
    animation: turn 60s linear infinite;
  }

  .minute {
    width: 3px;
    height: 64px;
    background: #B6B6B6;
    position: absolute;
    left: 50%;
    top: -2px;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
    transform-origin: bottom;
    transform: translate(-50%) rotate(176deg);
    animation: turn 3600s steps(60, end) infinite;
    animation-delay: var(--_dm, 0ms);
  }

  .hour {
    width: 3px;
    height: 54px;
    background: #FFFFFF;
    position: absolute;
    left: 50%;
    top: 12px;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
    transform-origin: bottom;
    transform: translate(-50%) rotate(-24deg);
    animation: turn 43200s linear infinite;
    animation-delay: var(--_dh, 0ms);
  }

}

03.png

此时的效果已经很不错了,但是如果我们想要让这个时钟真的动起来,则还需要一步!

让时钟动起来!

其实到了这一步我们这盘饺子已经算是包完了,但是最后的动画效果就是最后的醋!没有醋他难受啊!

秒针

我们以秒针为例转一圈的时间需要60s,那我们直接让秒针围绕中心点做一个旋转360°的动画岂不是就非常完美!


 .second {
    width: 4px;
    height: 84px;
    background: #FFAC0B;
    position: absolute;
    left: 50%;
    top: -18px;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
    transform-origin: bottom;
    transform: translate(-50%) rotate(45deg);
    animation: turn 60s linear infinite;
  }

  @keyframes turn {
  to {
    transform: translate(-50%) rotate(1turn);
  }
}

01.gif

分针

完美!接下来是分针分针转一圈是一个小时也就是60分钟60 * 60 = 3600s所以我们的分针动画应该这样写

  .minute {
    width: 3px;
    height: 64px;
    background: #B6B6B6;
    position: absolute;
    left: 50%;
    top: -2px;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
    transform-origin: bottom;
    transform: translate(-50%) rotate(176deg);
    animation: turn 3600s steps(60, end) infinite;
  }

animation: turn 3600s steps(60, end) infinite;中的steps(60, end)是动画的缓动函数,表示动画在 60 个步骤中完成,end 表示在每一步的结束时更新(跳到下一个状态)。

  • 使用 steps(60, end) 会使动画在 60 个固定的时间点发生变化,而不是平滑地进行。每一步都是一帧,效果类似于时钟每分钟跳动一次。
  • 例如,如果动画是旋转的,那么这会导致旋转效果像是分段式的,每 1 分钟转动一次,总共 60 步。

时针

时针转一圈是12小时也就是12 * 60 * 60 = 43200s

  .hour {
    width: 3px;
    height: 54px;
    background: #FFFFFF;
    position: absolute;
    left: 50%;
    top: 12px;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
    transform-origin: bottom;
    transform: translate(-50%) rotate(0deg);
    animation: turn 43200s linear infinite;
  }

02.gif

此时我们所有的指针都可以动起来了!但是还有一个问题就是其实时间都是从12:12:12开始,为了解决这个问题我们需要用到一个animation-delay特性

animation-delay

animation-delay: time;

如果time是负数则动画会立即开始执行且跳过一部分动画时间,相当于动画的进度已经进行了一部分。

例如:

  • 如果 time 为 -1s,则动画会立即开始播放,但相当于动画已经进行了一秒钟的位置。
  • 这意味着动画会直接从延迟指定的负数时间后的位置开始,而不会从起点开始。

    const time = new Date();
    const mins = -60 * time.getMinutes();
    const hand = document.querySelector('.hand')
    hand.style.setProperty('--dm', `${mins}s`);

利用这个特性我们已知分针转一圈需要60steps,我们-60 * 当前分钟就可以让动画提前跳转到分针的位置

效果展示!

结语

刚过完十一不想工作啊!!!!!