CSS 繪製一個時鐘

XboxYan發表於2022-04-25
歡迎關注微信公眾號:前端偵探

練習 CSS 一個很好的方法就是繪製各式各樣的 UI,比如這樣一個時鐘?

Kapture 2022-03-27 at 14.10.51

你也可以訪問這個 CSS clock檢視實際效果

CSS 繪製這樣一個佈局有幾個難點:

  1. 環形排列的刻度
  2. 環形分佈的數字
  3. 自動執行的指標

下面就來一一實現它,相信能學到很多 CSS 繪製和動畫的小技巧

一、環形排列的刻度

提到“環形”,可以想到錐形漸變 conic-gradient。假設有這樣一個容器

<clock></clock>

加上一點錐形漸變

clock{
  width: 300px;
  height: 300px;
  background: conic-gradient(#333 0 15deg, #cddc39 0deg 30deg);
}

可以得到這樣的效果

image-20220327143656931

如何做出交錯相間的效果呢?可以試試 repeating-conic-gradient

clock{
  /**/
  background: repeating-conic-gradient(#333 0 15deg, #cddc39 0deg 30deg);
}

效果如下

image-20220327143943041

還是看不出和刻度有啥關係?沒關係,我們把黑色部分的角度改小一點

clock{
  /**/
  background: repeating-conic-gradient(#333 0 1deg, #cddc39 0deg 30deg);
}

效果如下

image-20220327144256400

這樣繪製出來的幾條線是不是剛好可以對應時鐘的刻度?

然後將整個形狀變成圓環,可以用 MASK 來實現,實現如下

clock{
  /**/
  border-radius: 50%;
  -webkit-mask: radial-gradient(transparent 145px, red 0);
}

效果如下

image-20220327144635739

其實,這裡還有一個小細節,黑色部分並不是居中的,需要修正一下(可以更改起始角度,指定 from)。然後,將這個草綠色換成透明就可以了,完整程式碼如下

clock{
  /**/
  background: repeating-conic-gradient(from -.5deg, #333 0 1deg, transparent 0deg 30deg);
  border-radius: 50%;
  -webkit-mask: radial-gradient(transparent 145px, red 0);
}

最終效果

image-20220327145225829

分鐘的刻度也是同樣的道理,因為共有 60 個刻度,所以最小角度是 6 度(360 / 60),實現如下

clock{
  /**/
  background: repeating-conic-gradient(#333 0 1deg, #cddc39 0deg 6deg);
}

image-20220327145530128

利用 CSS 背景可以無限疊加的特性,可以將這兩個背景繪製在同一個元素下,所以完整程式碼如下

clock{
  /**/
  background: repeating-conic-gradient(from -.5deg, #333 0 1deg, transparent 0deg 30deg), 
    repeating-conic-gradient(from -.5deg, #ccc 0 1deg, transparent 0deg 6deg);
  border-radius: 50%;
  -webkit-mask: radial-gradient(transparent 145px, red 0);
}

最終錶盤刻度效果如下

image-20220327145858277

二、環形分佈的數字

看到這種佈局,我的第一反應其實是 textPath,這個 SVG 元素可以讓文字沿著指定路徑進行排列,比如下面這個 MDN 上的例子

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <path id="MyPath" fill="none" stroke="red"
        d="M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50" />
  <text>
    <textPath href="#MyPath">
      Quick brown fox jumps over the lazy dog.
    </textPath>
  </text>
</svg>

效果如下

image-20220327151311685

但是,這種方式有一個缺陷,無法改變文字的角度,只能沿著路徑垂直方向,而時鐘的數字方向都是正常的。

經過一番琢磨,發現還有一種方式也有類似沿著路徑的佈局方式,那就是 offset-path! 下面是 MDN 上的一個演示效果

Kapture 2022-03-27 at 15.19.43

那麼和環形排列數字有什麼關係呢?假設有這樣一個佈局

<clock-pane>
    <num>1</num>
</clock-pane>

然後將這個數字指定到一個圓形的路徑上(目前僅支援 path

num{
  offset-path: path('M250 125c0 69.036-55.964 125-125 125S0 194.036 0 125 55.964 0 125 0s125 55.964 125 125z');
}

效果如下(起始點是跟隨 path 路徑的)

image-20220327152419525

然後,可以通過 offset-distance來改變元素在路徑上的位置,並且支援百分比

num{
  offset-path: path('M250 125c0 69.036-55.964 125-125 125S0 194.036 0 125 55.964 0 125 0s125 55.964 125 125z');
  offset-distance: 100%
}

下面是從 0 到 100% 的變化

Kapture 2022-03-27 at 15.27.23

預設情況下元素的角度也是自適應垂直於路徑的,和 textPath 比較類似。但是我們可以手動指定固定角度,需要 offset-rotate,指定為 0deg 就行了

num{
  offset-path: path('M250 125c0 69.036-55.964 125-125 125S0 194.036 0 125 55.964 0 125 0s125 55.964 125 125z');
  offset-rotate: 0deg;
  offset-distance: 100%
}

效果如下,角度已經完全歸正了

Kapture 2022-03-27 at 15.33.07

如果有類似的佈局需求,是不是可以參考這個案例呢?

接下來,我們通過 CSS 變數,把 12 個數字自動歸位到指定位置

<clock-pane>
  <num style="--i:1">1</num>
  <num style="--i:2">2</num>
  <num style="--i:3">3</num>
  <num style="--i:4">4</num>
  <num style="--i:5">5</num>
  <num style="--i:6">6</num>
  <num style="--i:7">7</num>
  <num style="--i:8">8</num>
  <num style="--i:9">9</num>
  <num style="--i:10">10</num>
  <num style="--i:11">11</num>
  <num style="--i:12">12</num>
</clock-pane>

配合 calc 計算,完整程式碼如下

num{
  position: absolute;
  offset-path: path('M250 125c0 69.036-55.964 125-125 125S0 194.036 0 125 55.964 0 125 0s125 55.964 125 125z');
  offset-distance: calc( var(--i) * 10% / 1.2 - 25%);
  offset-rotate: 0deg;
}

效果如下

image-20220327153707225

三、自動執行的指標

三個指標的繪製應該沒有太大的難度,假設結構如下

<hour></hour>
<min></min>
<sec></sec>

需要注意一下旋轉的中心

hour{
  position: absolute;
  width: 4px;
  height: 60px;
  background: #333;
  transform-origin: center bottom;
  transform: translateY(-50%) rotate(30deg);
}
min{
  position: absolute;
  width: 4px;
  height: 90px;
  background: #333;
  transform-origin: center bottom;
  transform: translateY(-50%) rotate(60deg);
}
sec{
  position: absolute;
  width: 2px;
  height: 120px;
  background: red;
  transform-origin: center bottom;
  transform: translateY(-50%) rotate(90deg);
}
sec::after{
  content: '';
  position: absolute;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  left: 50%;
  bottom: 0;
  background: #fff;
  border: 4px solid #333;
  transform: translate(-50%, 50%);
}

效果如下

image-20220327154441471

到這一步靜態佈局就算完成了,那麼如何執行呢?

之前在這篇文章 [還在使用定時器嗎?CSS 也能實現電子時鐘]() 有詳細講解到,不借助定時器,也完全可以用 CSS 動畫來實現!

回到這裡,執行的原理很簡單,就是一個無限迴圈的 CSS 動畫,如下

@keyframes clock {
  to {
    transform: translateY(-50%) rotate(360deg);
  }
}

不同的是,時針、分針、秒針的週期不一樣,時針轉一圈是 12 小時、分針是 60 分鐘、秒針是 60 秒,各自需要換算成 秒數(CSS 單位只支援 毫秒),為了方便測試,這裡將速度調快了 60s → 6s

程式碼實現就是(--step 是一分鐘)

hour{
  /**/
  transform: translateY(-50%) rotate(0);
  animation: clock calc(var(--step) * 60 * 12) infinite;
}
min{
  /**/
  transform: translateY(-50%) rotate(0);
  animation: clock calc(var(--step) * 60) infinite;
}
sec{
  /**/
  transform: translateY(-50%) rotate(0);
  animation: clock var(--step) infinite;
}

效果如下

Kapture 2022-03-27 at 15.56.33

是不是有些奇怪?秒針在旋轉時先慢慢變快,然後又慢慢變慢,這是由於預設的動畫函式是ease,所以需要改成linear

sec{
  /**/
  animation: clock var(--step) infinite linear;
}

Kapture 2022-03-27 at 16.02.56

這樣就好多了。不過平時所見的時鐘,秒針通常都那種走一下,停下的,還有一種“滴答滴答”的節奏感,並不是這種無縫的。在 CSS 動畫中,是不是有點像階梯狀,沒錯,可以用到 CSS 的 steps 函式,不瞭解這個的可以參考張老師的這篇文章:CSS3 animation屬性中的steps功能符深入介紹,實現如下

sec{
  /**/
  animation: clock var(--step) infinite steps(60);
}

效果如下

Kapture 2022-03-27 at 16.10.25

接下來需要通過 JS 初始化時間,僅此而已。需要注意的是,在獲取的時候加上偏移量,比如 12:30,分針其實是 12.5,以此類推。程式碼實現如下

const d = new Date()
const h = d.getHours();
const m = d.getMinutes();
const s = d.getSeconds();
clock.style.setProperty('--ds', s)
clock.style.setProperty('--dm', m + s/60)
clock.style.setProperty('--dh', h + m/60 + s/3600)

然後 CSS 中可以通過 animation-delay來指定動畫的起始位置

hour{
  /**/
  animation: clock calc(var(--step) * 60 * 12) infinite linear;
  animation-delay: calc( -1 * var(--step) * var(--dh) * 60);
}
min{
  /**/
  animation: clock calc(var(--step) * 60) infinite linear;
  animation-delay: calc( -1 * var(--step) * var(--dm));
}
sec{
  /**/
  animation: clock var(--step) infinite steps(60);
  animation-delay: calc( -1 * var(--step) * var(--ds) / 60 );
}

然後加點輪廓裝飾,就實現了文章開頭的效果

Kapture 2022-03-27 at 14.10.51

完整程式碼可以檢視 CSS clock

四、簡單總結一下

以上就是CSS 繪製時鐘的全部過程了,本文更側重於繪製過程,特別是是環形佈局技巧,這裡簡單總結一下

  1. 碰到環形圖案可以想到 conic-gradient
  2. 環形刻度繪製的原理是 conic-gradient 和 MASK 裁剪
  3. 文字沿路徑排列可以使用 textPath
  4. textPath 不支援改變文字角度
  5. offset-path 可以讓元素沿指定路徑進行佈局
  6. offset-path 可以設定元素在路徑的偏移和角度
  7. 時鐘自動執行的原理的是 CSS 動畫
  8. 時鐘、分鐘、秒鐘的區別是動畫時長不同
  9. “滴答滴答”的效果可以用 steps 實現
  10. 時間初始化可以通過 animation delay 實現

當然本文重點並不僅僅是在於實現了一個時鐘,而是在於 CSS 繪製技巧的積累和掌握。有了相關的積累,以後在碰到類似的佈局,在腦子裡過濾一下,馬上就能找到解決方案。最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤

歡迎關注微信公眾號:前端偵探

相關文章