做一個會動的時鐘. 沒錯, 一個能夠顯示現在時間的時鐘.
一個時鐘會有什麼呢? 鐘面, 時針, 分針, 秒針, 鐘心.
HTML結構如下, 來吧!
<div class="clock">
<div class="clock-face">
<div class="hand hour-hand"></div>
<div class="hand min-hand"></div>
<div class="hand second-hand"></div>
</div>
</div>
來點CSS設定, 首先我們希望鐘外觀是圓的! border-radius: 50%
可以做到這點. padding: 2rem
讓鐘面和時鐘保持間距, 指針才不會碰到邊緣
.clock {
width: 30rem;
height: 30rem;
border: 20px solid black;
border-radius:50%;
padding: 2rem;
}
鐘面是指針們的活動空間, 用 width
, height
將剩餘空間撐滿. 接著設定 position: relative
.
為什麼呢? postion: absolute
為絕對定位, 被絕對定位的元素發生兩件事, 第一件事, 它會脫離Normal Flow, 不會像正常的block一樣佔空間, 往下堆疊. 第二件事, 它的定位從此只會以離他最近的父層且非position: static
的元素做為基準.
待會內部指針, 鐘心會使用 position: absolute
定位, 將以鐘面作為定位基準.
.clock-face {
position: relative;
width: 100%;
height: 100%;
}
接下來是所有指針的初始設定, 寫在hand
類別內.
指針定位設為position:absolute
, 根據上述內容, 定位參考元素為具有clock-face
類別的元素.
因此width: 50%
會讓所有指針預設都是鐘面寬度的一半.
注意定位的原點皆為元素的最左上角, 當設定top: 50%
時, 指針們上緣會距離clock-face
內部上緣約鐘面高度50%的距離.
現在所有指針會停在九點鐘方向的位置.
慢著, 這樣指針們不就沒有準確置中了嗎?
.hand {
width:50%;
height:6px;
background:black;
position: absolute;
top:50%;
}
好問題, 因為指針們有各自的寬度, 我們會在各自的類別中用transform: translateY()
調整, 讓指針們個別上移自己寬度的一半, 這樣就完全置中了!
.hour-hand {
width: 30%;
left: 20%;
transform: translateY(-3px);
}
.min-hand {
transform: translateY(-3px);
}
.second-hand {
background-color: red;
transform: translateY(-1px);
height: 2px;
}
transform
可以讓元素產生空間變形的效果, 例如放大, 旋轉, 翻轉...
說到 transform如何變形的 ,就要稍微提一下Stacking Context.
正常的Stacking Context, 可以把瀏覽器視窗內想像成一個3D的座標空間, X軸方向向右, Y軸方向向下, Z軸方向由螢幕內指向螢幕外, 元素按照Z軸的方向在堆疊.
對某個元素給了transform
屬性並指定變形方式時, 會產生一個小型的Stacking Context包裹住該元素, 接著根據變形的方式, Stacking Context的座標軸會被變形, 裡面的元素因為座標軸發生改變而變形.
聽起來很抽象, 用比喻的話, 把水裝在氣球裡面, 你捏氣球, 水因為氣球的形狀改變, 也跟著改變了!
在未載入程式碼前, 我們希望指針都指向12點鐘方向, 因此還需要加點好料...
.hand {
/* 前略 */
transform: rotate(90deg);
transform-origin: right;
}
transform-origin: right
將旋轉的支點移到指針右邊, 就是鐘面中心, 若不指定, 預設旋轉支點是元素的中心. transform: rotate(90deg)
把指針往順時針方向轉90度. 負值會往逆時針方向轉. 初始畫面完成了! 接下來就要加入程式碼了!
首先選取時針, 分針, 秒針, 然後將其存到變數內, 方便我們之後做操控.
// 時針, 分針, 秒針
const hrHand = document.querySelector(`.hour-hand`);
const minHand = document.querySelector(`.min-hand`);
const secHand = document.querySelector(`.second-hand`);
接著, 設定計時器, 每秒鐘執行一次自訂的回呼函數updateTime
, 更新指針的位置.
setInterval是window物件的全域方法, 裡面兩個參數, 第一個參數是每過一段時間要執行的函式, 第二個參數是時間間隔.
setInterval一但設定了, 就會一直跑. 若希望將來能夠手動移除該計時器, 可以將其指定給一個變數, 例如tick
, 然後用clearInterval(tick)
就能移除!
const tick = setInterval(updateTime, 1000);
我們希望時鐘的指針每秒能存取當下的時間, 並且更新到正確的位置, 所以updateTime
函式的內容如下:
寫完大概如下:
function updateTime() {
const now = new Date();
const hr = now.getHours();
const min = now.getMinutes();
const sec = now.getSeconds();
const hrDeg = hr * 360/12 + min * 360/12/60 + sec * 360/12/60/60 + 90;
const minDeg = min * 360/60 + sec * 360/60/60 + 90;
const secDeg = sec * 360/60 + 90;
hrHand.style.transform = `translateY(-3px) rotate(${hrDeg}deg)`;
minHand.style.transform = `translateY(-3px) rotate(${minDeg}deg)`;
secHand.style.transform = `translateY(-1px) rotate(${secDeg}deg)`;
}
Date物件記錄了任何有關當下時間的資訊, 要取得物件只能以建構式實例化(intantiate)的方式執行. 像這樣const now = new Date()
, Date()
就是Date物件的建構式, 用new
運算子將其實例化. 用比喻來說, 建構子就像工廠模具, 被實例化的物件就像產品, new
運算子就像工廠的機器這樣吧! 總之每次 new Date()
都會產生一個當下的時間就對了!
getHours()
, getMinutes()
, getSeconds()
分別為Date物件的三種方法, 用來取得當下時間的小時, 分鐘, 秒的值. 但是得到的值都是從0開始算, 例如3:12:52, 會得到2, 11, 51這三個數字.
要轉換成度數就是小學高年級課本教過的時鐘問題了, 好懷念! 鐘面旋轉一圈是360度, 秒針有60格, 1格6度, 所以當下秒數換成度數就是... 秒數 x 360/60
度! 以此類推...
但因為我們未transform
前, 指針的起點是在9點鐘方向, 因此完成計算後, 個別要再加上90度, 才會顯示正確的時間!
最後用DOM物件的style
屬性存取transform
特性, 把旋轉度數更新成計算出來的值就好.
因為這種存取transform
特性的方式是重新指定一個值給它, 因此舊的值會被洗掉, 記得我們之前有針對指針置中的問題做微調, 這裡要重複打一次上去. 這樣才能保證每次更新位置時, 指針都有置中.
指針雖然有在動, 但好像沒有tick tock tick tock的感覺, 身為一個古董發條機械鐘領域的權威, 各位肯定覺得這個鐘不合格! 沒關係, 讓我們在每個指針的CSS引入transition
轉場. 第一篇有提到轉場的作用就是讓特性變化的過程有連續性. 轉場的其中一個參數是控制轉場速率跟時間的相依性, 但預設的選擇其實不多. 想要更客製化的變化, 我們可以用transition-timing-function
特性來自訂這個時間曲線, 希望指針有跳動的感覺, 可以像下面這樣設定.
.hand {
transition: all 0.05s;
transition-timing-function: cubic-bezier(0.1, 2.7, 0.58, 1);
}
設置完轉場後, 看起來更像時鐘了!鐘錶界的權威們普天同慶. 但如果各位像我一樣會陶醉在成品中盯著看好幾個小時, 一定會發現一個BUG!
沒錯, 為什麼指針轉一圈後會自動倒退一圈呢?
問題出在這裡...
const secDeg = sec * 360/60 + 90;
secHand.style.transform = `translateY(-1px) rotate(${secDeg}deg)`;
當指針從 59 到 0 的瞬間, 依照公式, 度數會從444度瞬間變回90度, 視覺上看來雖然是從84度到90度的差距而已, 但數值上來看是整整後退了快360度.
因為我們用了CSS轉場, 所以旋轉的會在444度到90度的過程中建立一個連續動畫, 所以才會有倒轉一圈的動畫!
該怎麼修正這個問題呢?
魔術數字就是444度! 沒錯, 即使超過360度, 時鐘仍是不停運轉著, 我們只要不歸零, 讓數字持續跑下去, 就不會倒轉一圈了!
程式碼如下:
// 在函數外設定一個變數
/* 前略 */
let round = 0;
function updateTime() {
/* 前略 */
// 在 const sec = now.getSeconds() 後面加上
if(sec === 0) {
round++;
}
/* secDeg稍微改一下 */
let secDeg = (sec + 60*round) * 360/60 + 90;
}
在函數外增加一個全域變數 round, 負責記錄現在轉幾圈了.
在函數內偵測只要秒數歸零, 也就是轉了一圈, 就讓round圈數增加.
在實際計算度數時, 看轉了幾圈, 就加上幾個360度, 這麼一來, 度數就會持續前進. 也就不會看到迴轉的問題.
其實這只是一個勉勉強強的解法, 因為如果放著不管讓網頁一直跑下去, 總有一天會因為數字過大而溢位. 嗯... 如果之後有發現什麼更好的解法, 再寫一篇補充上來吧!
以上就是JS30 第二篇心得!
這個時鐘,我想研究一下。
看看能不能倒數:
一個填滿顏色的圓,
隨時間,慢慢的減少填滿的面積
做出來時分享一下啊~