還在使用定時(shí)器嗎?有點(diǎn)離譜的 CSS 電子時(shí)鐘

通常要做一個(gè)時(shí)鐘,肯定離不開 JS 定時(shí)器。今天換一種思路,用 CSS 來實(shí)現(xiàn)一個(gè)時(shí)鐘,如下:

你也可以訪問這個(gè)CSS time (codepen.io)[1]查看實(shí)際效果。
當(dāng)然借用了一點(diǎn)點(diǎn) JS 用于初始化時(shí)間,整個(gè)時(shí)鐘的運(yùn)行都是由 CSS 完成的,有很多你可能不知道的小技巧,一起看看吧。
一、數(shù)字的變換
先看看數(shù)字是如何變換的。
在以前,如果要實(shí)現(xiàn)數(shù)字的遞增變化,可能需要提前準(zhǔn)備好這些數(shù)字,例如像這樣。
然后通過改變位移來實(shí)現(xiàn)。
但是,現(xiàn)在有更簡(jiǎn)潔的方式可以實(shí)現(xiàn)了,那就是 CSS @property[2],不了解這個(gè)的可以參考這篇文章:CSS @property,讓不可能變可能[3]。這是干什么的呢?簡(jiǎn)單來講,可以自定義屬性,在這個(gè)例子中,可以讓數(shù)字像顏色一樣進(jìn)行過渡和動(dòng)畫,可能不太懂,直接看例子吧。
假設(shè) HTML 是這樣的。
我們讓這個(gè)自定義變量在頁面中展示出來,單純的 content無法直接顯示自定義變量,需要借助定時(shí)器,有興趣的可以參考這篇文章:小tips: 如何借助content屬性顯示CSS var變量值[4]。
span::after{
counter-reset: num var(--num);
content: counter(num);
}

然后,可以通過:hover改變這個(gè)數(shù)字。
span:hover::after{
--num: 59
}

很生硬的從 0 變成 59 了,非常符合常規(guī)。如果利用 CSS property,情況就不一樣了,需要改造的地方很少,先定義一下--h,然后給這個(gè)變量一個(gè)過渡時(shí)間,如下:
@property --h {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
span::after{
transition: 1s --num;
}
神奇的一幕發(fā)生了

看著好像不可思議?可以這么理解,通過@property定義后,這個(gè)變量本身可以單獨(dú)設(shè)置過渡了,而不再取決于一些僅支持過渡的屬性(color、width等)。甚至還能加上動(dòng)畫,需要用到steps方法,設(shè)置動(dòng)畫周期為無限,如下:
@keyframes num {
to {
--num: 10
}
}
span{
animation: num 1s infinite steps(10);
}
時(shí)鐘的基本運(yùn)行原理就是這樣了,一個(gè)無限循環(huán)的 CSS 動(dòng)畫!

二、時(shí)、分、秒
下面來看具體時(shí)、分、秒的實(shí)現(xiàn),HTML 如下:
<div class="time">
<span class="hour"></span>
<a class="split">:</a>
<span class="minitus"></span>
<a class="split">:</a>
<span class="seconds"></span>
</div>
給時(shí)、分、秒附上初始值。
@property --h {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@property --m {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@property --s {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
.hour::after{
counter-reset: hour var(--h);
content: counter(hour);
}
.minitus::after{
counter-reset: minitus var(--m);
content: counter(minitus);
}
.seconds::after{
counter-reset: seconds var(--s);
content: counter(seconds);
}

這里的時(shí)、分、秒并沒有聯(lián)動(dòng)關(guān)系,所以各自都需要單獨(dú)的動(dòng)畫。下面就需要思考一下??,如果用 CSS 動(dòng)畫來實(shí)現(xiàn),每個(gè)的動(dòng)畫起始點(diǎn)和時(shí)長(zhǎng)是多少呢?
沒錯(cuò),就是你想的,時(shí)針是0-23,時(shí)長(zhǎng)24h,分針是0-59,時(shí)長(zhǎng)60min,秒針是0-59,時(shí)長(zhǎng)60s,但是 CSS 中的時(shí)間單位只支持秒(s)或者毫秒(ms),所以這里需要轉(zhuǎn)換一下,時(shí)長(zhǎng)分別是60s*60*24、60s*60、60s,具體實(shí)現(xiàn)如下:
@keyframes hour {
to {
--h: 24
}
}
@keyframes minitus {
to {
--m: 60
}
}
@keyframes seconds {
to {
--s: 60
}
}
.hour::after{
counter-reset: hour var(--h);
content: counter(hour);
animation: hour calc(60s * 60 * 24) infinite steps(24);
}
.minitus::after{
counter-reset: minitus var(--m);
content: counter(minitus);
animation: minitus calc(60s * 60) infinite steps(60);
}
.seconds::after{
counter-reset: seconds var(--s);
content: counter(seconds);
animation: seconds 60s infinite steps(60);
}
這里為了便于觀察,將時(shí)間調(diào)快了10倍(60s => 6s),如下

三、時(shí)、分、秒自動(dòng)補(bǔ)零
上面的布局有個(gè)問題,1 位數(shù)和 2 位數(shù)寬度變化導(dǎo)致時(shí)鐘整體都在“晃動(dòng)”,所以需要在1位數(shù)時(shí)補(bǔ)上一個(gè)“0”。關(guān)于 CSS 補(bǔ)零,之前在文章 CSS 補(bǔ)全字符串?中提到了 3 種方案,由于這里用了計(jì)數(shù)器,所以直接選擇更改計(jì)數(shù)器樣式的方法,通過decimal-leading-zero來實(shí)現(xiàn),具體做法如下:
.hour::after{
/**/
content: counter(hour, decimal-leading-zero);/*添加計(jì)數(shù)器樣式*/
}
這樣就和諧多了

四、時(shí)間初始化
剛才都從00:00:00開始了,所以需要手動(dòng)指定一下初始時(shí)間。假設(shè)現(xiàn)在是19:26:30,如何初始化呢?
這里需要用animation-delay來提前運(yùn)動(dòng)到未來指定位置,為了方便控制,使用三個(gè)變量--dh、--dm、--ds來表示初始時(shí)間,注意,由于animation-delay也只支持秒(s)或者毫秒(ms),所以也同樣需要轉(zhuǎn)換,實(shí)現(xiàn)如下
:root{
--dh: 19;
--dm: 26;
--ds: 30;
}
.hour::after{
/**/
animation: hour calc(60s * 60 * 24) infinite steps(24);
animation-delay: calc( -60s * 60 * var(--dh) );
}
.minitus::after{
/**/
animation: minitus calc(60s * 60) infinite steps(60);
animation-delay: calc( -60s * var(--dm) );
}
.seconds::after{
/**/
animation: seconds 60s infinite steps(60);
animation-delay: calc( -1s * var(--ds) );
}

是不是有點(diǎn)奇怪?分鐘在秒鐘走到 30 的時(shí)候才變化,晚了半分鐘。原因是這樣的,雖然從數(shù)字上看,分鐘是 26,但是還要考慮到秒鐘的運(yùn)動(dòng)情況,比如像這種情況,分鐘其實(shí)已經(jīng)走了一半,應(yīng)該是26.5(26 + 30 / 60),所以在計(jì)算時(shí)還需要加上偏移量。下面我們通過 JS 獲取真實(shí)的時(shí)間,并修復(fù)偏移
const d = new Date()
const h = d.getHours();
const m = d.getMinutes();
const s = d.getSeconds();
document.body.style.setProperty('--ds', s)
document.body.style.setProperty('--dm', m + s/60)
document.body.style.setProperty('--dh', h + m/60 + s/3600)
這樣就正常了

五、閃爍的分隔符
為了時(shí)鐘看起來更加“動(dòng)感”,可以給分隔符加上閃爍動(dòng)畫,代碼如下:
@keyframes shark {
0%, 100%{
opacity: 1;
}
50%{
opacity: 0;
}
}
.split{
animation: shark 1s step-end infinite;
}
現(xiàn)在看下最終的效果

完整代碼可以訪問 CSS time (codepen.io)[5]
六、總結(jié)一下
想不到實(shí)現(xiàn)一個(gè)時(shí)鐘效果,用到了那么多 CSS 知識(shí)和技巧,簡(jiǎn)單總結(jié)一下吧
- CSS 實(shí)現(xiàn)本質(zhì)是無限循環(huán)的 CSS 動(dòng)畫
- 靈活運(yùn)用 CSS calc 計(jì)算
- CSS 計(jì)數(shù)器可以將 CSS 變量通過 content 顯示在頁面
- 數(shù)字的變化現(xiàn)在可以通過 CSS @property 配合動(dòng)畫實(shí)現(xiàn)
- 時(shí)分秒的區(qū)別在于各自的動(dòng)畫時(shí)長(zhǎng)、動(dòng)畫起始點(diǎn)不同
- CSS 自動(dòng)補(bǔ)零可以參考之前的文章,這里采用 decimal-leading-zero 實(shí)現(xiàn)
- 時(shí)間初始化其實(shí)就是指定動(dòng)畫 delay 值
- 指定初始值時(shí)還需要考慮到各自的偏移量,例如 19:30:30,此時(shí)的時(shí)針數(shù)字其實(shí)是 30.5
- 分隔符的閃爍動(dòng)畫
其實(shí)整個(gè)實(shí)現(xiàn)過程就是一個(gè)不斷思考、學(xué)習(xí)的過程,比如為了實(shí)現(xiàn)數(shù)字的變化,就必須去學(xué)習(xí) @property 相關(guān),為了實(shí)現(xiàn)補(bǔ)零,就需要去了解更深層次的計(jì)數(shù)器相關(guān),還有用到的各種動(dòng)畫。最后,如果覺得還不錯(cuò),對(duì)你有幫助的話,歡迎點(diǎn)贊、收藏、轉(zhuǎn)發(fā)???

























