JS 地下城挑戰 - 2F 時鐘

圖片來源: 六角學院

前言

這關 UI 的部分可以選擇用 CSS 手刻或是直接使用現成的圖片。大部分的時間在處理 UI 的部分,有一個關鍵的 CSS 屬性 transform-origin,稍後會介紹到,時鐘運作的 JavaScript 相對來說就比較不這麼繁瑣,做一些數學加減運算,配合 Date物件 取得時分秒參數即可。

Demo
Github

關卡資訊

UI設計稿

  • 【特定技術】需使用 JS 原生語法的 getDate() 撈取時間,不可用套件
  • 【特定技術】需使用 JS 原生語法的 setTimeout()setInterval(),持續讓秒針、分針、時針能夠以台北時區移動
  • 【特定技術】介面請全部用 CSS2CSS3 手寫繪製,什麼…?你說太強人所難??那..用圖片也不是不行辣
  • 你攻略此 BOSS 的攻略過程心得

解題

時鐘輪廓

將時鐘的輪廓刻出來

HTML

1
2
3
<div class="clock">
<div class="clock__inner"></div>
</div>

SCSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
%centerJustify {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.clock {
width: 350px;
height: 350px;
border-radius: 100%;
@extend %centerJustify;
background-color: #a06ee1;
box-shadow: 8px 8px 15px #0000007d;

&__inner {
width: 90%;
height: 90%;
border-radius: 100%;
position: relative;
@extend %centerJustify;
border: 2px solid #212F0B;
background-color: #421b9b;
}
}

時鐘刻度

接著我們需要把刻度做出來,先不管刻度的樣式和大小,我們知道時鐘總共有 60 個刻度,但是這個設計稿有加一些設計在裡面,每個小時的刻度內各多一個菱形的刻度設計,所以總共的刻度有 60+12=72 個。再來我們需要算出每個小刻度佔幾度,一個圓 360 度,除以刻度數量,得一個刻度為 360/72=5

來寫刻度的 CSS 吧,先設定一個父元素 .scale,裡面會有 72個小刻度 .scale__unit,不過之後我們會使用 JS 迴圈方式來跑 72 次,不會寫死在 HTML 中,所以先寫一個來設定 CSS 樣式即可。

1
2
3
4
5
6
7
<div class="clock">
<div class="clock__inner">
<div class="scale">
<div class="scale__unit"></div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.scale {
position: relative;
width: 100%;
height: 100%;

&__unit {
transform: rotate(180deg) translateY(120px); // 由於每個刻度的rotate角度不同,這個 CSS 之後會透過 JS 來產生。
width: 2px;
height: 24px;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform-origin: 0 0;
background-color: #fff;
}
}
`

transform-origin

上面的程式碼有一個關鍵語法 transform-origin,可以帶入兩個參數( % ),分別代表 X 與 Y ,這個語法可以改變元素transform 的基準點,若沒有特別設定在 CSS 中,預設的基準點為元素的正中心(下圖第一個例子)。可以參考下面兩個連結,會更容易理解。

transform-origin Demo
clock Demo

時鐘刻度處理及細節

跑迴圈產生 72 個刻度 DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function renderClockUnit() {
const unit_wrap = document.querySelector('.scale'); // 父元素 DOM
let deg = 180; //刻度的旋轉角度,180度為12時的位置

for (let i = 0; i<72; i++) {
const unit = document.createElement('div'); // 創造刻度DOM
unit.classList.add('scale__unit');

// 小時的刻度處理
if(i%6 == 0) {
unit.style.transform = `rotate(${deg}deg) translateY(110px)`;
// 其他刻度處理
} else {
unit.style.transform = `rotate(${deg}deg) translateY(120px)`;
}

deg += 5;
unit_wrap.appendChild(unit);
}
}

小時刻度 & 裝飾刻度樣式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.scale__unit:nth-child(6n+1) {
height: 24px;
width: .5px;
background-color: #cef9e2;
}

.scale__unit:nth-child(6n+4)::before {
content: '★';
display: block;
position: relative;
left: -3px;
top: -7px;
color: #CEF9E2;
font-size: 2px;
}

到這步,時鐘的刻度算是處理完囉!

製作時鐘上的數字

設計稿上有二十四時制及十二時制的時間,我們先宣告兩個變數分別代表它們

1
2
let twelve_hours = 0;    // 十二時制
let twenty_hours = 12; // 二十四時制

接著,只有在小時刻度時會有數字顯示, 接續上面的程式碼,if 陳述句 i%6 == 0 即代表每小時刻度,我們在該條件成立時需渲染時鐘上的數字。
渲染起始點為 12 點的位置,因此當 twelve_hour 跑第一次迴圈時應該為 12twenty_hours 則為 24,其他情況則帶入變數,且每次迴圈變數都要 +1

根據上面的代碼,再加入新的功能

1
2
3
4
5
6
7
8
9
    if(i%6 == 0) {
unit.innerHTML = `
<span class="twelve">${ i == 0 ? 12 : twelve_hours }</span>
<span class="twenty">${ i == 0 ? 24 : twenty_hours }</span>
`;
twelve_hours++;
twenty_hours++;
}
}

i == 0 ? 12 : twelve_hours 是 JS 的三元運算子,部份情況下可以用簡短的語句取代冗長的 if else ,結構為 [條件] ? [若true回傳此值] : [false則回傳此值]
我們可以解釋為,當 i == 0 ( 即第一次迴圈時 ),我要在 .twelve DOM 中輸出 12,其他的情況輸出 twelve_hours 變數當前的值。

CSS 樣式

1
2
3
4
5
6
7
8
9
10
11
.twelve, .twenty {
font-size: 10px;
position: absolute;
left: -5px;
}
.twelve {
top: -20px;
}
.twenty {
top: 25px;
}

到目前我們的時鐘長這樣,好像還差一點點

將時鐘的數字轉正

這.. 好像很簡單啊? 把 .twelve.twenty 旋轉 180度好像就可以了 ?

1
2
3
4
.twelve, .twenty {
/* ... 略 */
transform: rotate(180deg);
}

記得我們剛剛在處理刻度時,每一個小刻度轉了多少度嗎 ? 5度,而每一個小時間隔 6 個小刻度,記得吧? 如果要轉正該怎麼做呢 ?

先將剛剛 .twelve.twenty 的 CSS 刪除,我們用迴圈來產生 CSS,首先一樣迴圈起點為旋轉 180 度。

1
let time_deg = 180;

每小時間隔 6 個小刻度,每個小刻度為 5 度,因此一次迴圈需要將角度減去 6*5=30 度。

1
2
3
4
5
6
7
8
if(i%6 == 0) {
unit.innerHTML = `
<span class="twelve" style="transform:rotate(${ time_deg }deg)"> 略... </span>
<span class="twenty" style="transform:rotate(${ time_deg }deg)"> 略... </span>
`;

time_deg -= 30;
}

好的!目前的時鐘長這樣

製作指針

指針部分重點和刻度一樣,善用 transform-origin 來改變 transform 基準點即可。詳細製作過程就不在這邊敘述囉~ 請參考 UI 樣板

時鐘邏輯撰寫

UI 部分搞定了,接著來讓時鐘跑起來吧!!

將指針的 DOM 宣告起來

1
2
3
const hour_hand = document.querySelector('.hand-hour');
const min_hand = document.querySelector('.hand-min');
const sec_hand = document.querySelector('.hand-sec');

透過 JS 的 Date 物件抓取時分秒參數

1
2
3
4
const time = new Date();
const hour = time.getHours();
const min = time.getMinutes();
const sec = time.getSeconds();

再來是最關鍵的,將抓到的參數換算成 CSS 的角度,讓時鐘可以正常運作

  • 秒針
    sec 變數值的範圍介於 0 ~ 60(秒),圓為 360度,可以換算得每分鐘的角度為 360/60=6 度。最後需要再扣掉 180度,讓位置以 12 點鐘方向為起點。

    1
    const sec_deg = sec*6 - 180;
  • 分針
    分針的概念和秒針相似,不過分針另外還會受到秒數的影響,需另外加上 sec*6/60

    1
    const min_deg = min*6 + sec*6/60 - 180;
  • 時針
    每小時為 360/12=30度,另外加上分鐘數的影響 min*6/60即可。

    1
    const hour_deg = (hour*30 + min*30/60) - 180;

透過 transform: rotate 為指針 DOM 套用角度

1
2
3
hour_hand.style.transform = `rotate(${ hour_deg }deg)`;
min_hand.style.transform = `rotate(${ min_deg }deg)`;
sec_hand.style.transform = `rotate(${ sec_deg }deg)`;

setInterval 重複執行

將上面整理的邏輯包裝成函式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function clockPrimaryFeature() {
const hour_hand = document.querySelector('.hand-hour');
const min_hand = document.querySelector('.hand-min');
const sec_hand = document.querySelector('.hand-sec');

const time = new Date();
const hour = time.getHours();
const min = time.getMinutes();
const sec = time.getSeconds();

const hour_deg = (hour*30 + min*30/60) - 180;
const min_deg = min*6 + sec*6/60 - 180;
const sec_deg = sec*6 - 180;

hour_hand.style.transform = `rotate(${ hour_deg }deg)`;
min_hand.style.transform = `rotate(${ min_deg }deg)`;
sec_hand.style.transform = `rotate(${ sec_deg }deg)`;
}

執行

1
2
3
4
5
6
7
function runClock(callback){
renderClockUnit();
callback();
setInterval(callback, 1000);
}

runClock(clockPrimaryFeature);

Codepen Demo

See the Pen JS地下城 - 2F 時鐘 VanillaJS by Dylan (@Dylan_Liu) on CodePen.