アナログ時計を作ってみた
こんにちは、CTO室の秋田です。若手社員の研修を担当しています。IT全般いろんな研修をしていますが、主には新人対象なので浅く広く教えています。
あることがきっかけでアナログ時計を作ったのですが、日頃HTMLを書かないこともあって、CSSすっげ~と感動したので紹介します。
(本稿は朝日新聞社の2024年Qiitaアドベントカレンダーの投稿です)
CSSで動かすアナログ時計
できあがった時計がこれ。すっごくシンプル。目覚まし機能もラップ機能もありません。ただただ時刻を表示するのみです。
感動は、時計の針は基本はCSSが動かしているのです。実際にはJavaScriptでCSSを変えているのですが、ただただ感動したのです。
とりあえず時計を見る
ソースは300行弱ありますが、後にぼちぼちと説明します。
とりあえず本物を見たい人は、こちらをダウンロードしてブラウザーで表示してください。
時計の要求仕様
欲しい時計の仕様は、いたってシンプルです。
長針、短針、秒針があるアナログ時計
時分秒の目盛りがある
好みのサイズに簡単に変えられる
画面いっぱいの大きな表示もできる
1つのファイルで実現
アナログ時計の作り方
今回作ったアナログ時計の作り方を紹介します。作り方と言っても単なるHTMLなので、材料の切り方や部品の取り付け方じゃなくソースコードの説明です。
コードは、枠を作って、文字盤を作って、日付を表示して、時計の針を時刻に応じた場所に配置しています。コードを順に追うとこのようになっています。
この順に合わせて説明します。
コードの説明は、必要なところを抜き出しています。全体を見るには、ファイルをダウンロードして眺めてください。
HTMLの全体
1つのファイルで実現するのが目的なので、JavaScriptもCSSも1つのHTMLにぶち込んでいます。CSS、JavaScriptの中身を除くとこれだけです。
最終的にはアナログ時計だけじゃなく、おまけでデジタル時計も付けました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>とけい</title>
<style type="text/css">
** 略
</style>
</head>
<body>
<div class="clock">
<div class="clockFace"></div>
<div class="branchAmPm"></div>
<div class="dateArea"></div>
<div class="hand hour" id="hour"></div>
<div class="hand minute" id="minute"></div>
<div class="hand second" id="second"></div>
</div>
<div class="digital_clock"></div>
<script>
** 略
</script>
</body>
</html>
時計の枠を作成
時計の枠や部品のサイズは、最初に基準とする値を決めて、それを元に決めています。基準値はブラウザーのビューポート(vw)の幅を元に決めて、clocksizeという変数に保存しています。
今回の基準値は、ビューポートの幅の80%としました。この80%は、いくつか試して気に入ったのサイズです。
コードのこの部分です。
--clocksize: 80vw;
時計の枠は、縦横clocksizeのサイズの正方形を作って、角を丸めることで円にしています。
コードのこの部分です
width: var(--clocksize);
height: var(--clocksize);
border-radius: 50%;
●CSSの関連部分
<style type="text/css">
body {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
/* 時計全体の大きさ */
/* ウインドーのスクロール幅に対する割合で指定 */
--clocksize: 80vw;
background: #fff;
}
/* 時計の枠 */
.clock {
position: absolute;
top: 5%; /* これは画面上から下方向にずらす距離 */
border: 1px solid;
/*正方形を指定*/
width: var(--clocksize);
height: var(--clocksize);
/* 正方形を円に変換 */
border-radius: 50%; /* border-radius: 半径; 半径50%で円*/
}
</style>
●HTMLの関連部分
<body>
<div class="clock">
<div class="clockFace"></div>
</div>
</body>
文字盤を作成
目盛りや数字は、JavaScriptでdiv要素で設置するエリアをいっぱい作り、CSSのtransformプロパティのrotate(時計回りの回転)、translate(平行移動)を使って部品を配置しました。
まずは目盛りの作り方の紹介です。
目盛りは60個あるので、JavaScriptでループを使って5で割り切れるときは時刻用、他のときは分秒用の目盛りのためのエリアとなるdivタグを作っています。JavaScriptからループカウンターをCSSへ伝えることでCSSで位置を計算して目盛りを配置しています。
JavaScriptは、次の部分で<div class='mark_hour'>を作っています。
mark_hour = document.createElement('div');
mark_hour.className = 'mark_hour';
ここでループカウンターを変数jに入れてCSSに伝えています。
mark_hour.style.cssText = '--j:' + n + ';'; //cssの変数定義
CSSでは、クラスセレクター mark_hourで時刻の目盛り、mark_minuteで分の目盛りを配置しています。
まず次の指定で、文字盤の中心を座標変換の原点にしています。
transform-origin: right;
目盛りはループカウンターに応じて回転させ、配置場所は中心から時計枠の半径分離れたところから目盛りの長さの半分ずらしたところに配置しています。
この部分で目盛りを作成しています。
width: 2px; /* 目盛りの太さ */
--mark_minute_long : calc(var(--clocksize) * 0.05);
height: var(--mark_minute_long); /* 目盛りの長さ */
この部分で配置しています。
transform:
rotate(calc(30deg * var(--j)))
translate(0, calc(var(--clocksize) * -0.5
+ var(--mark_minute_long)/2)); }
次に数字部分の紹介です。
目盛りと同じで、目盛りの代わりに数字を配置すれば良いと思っていたのですが、こうすると目盛りが回転して角度を持って配置しているのと同様に、数字も回転して表示されてしまいます。
そこで数字を配置する領域を作って、その中に数字を配置して逆回転させました。
JavaScriptはこの部分で<div class='number_area'>を作っています。
number_area = document.createElement('div');
clockFace.appendChild(number_area);
このdivタグの中に<div class='number'>を作っています。
number = document.createElement('div');
number_area.appendChild(number);
CSSでは、クラスセレクター number_areaで数字を置く場所をJavaScriptのループカウンターに応じて決め、クラスセレクター numberで数字表示の傾きを戻しています。
transform: rotate(calc(-30deg * var(--i)));
これで、文字盤の完成です。
●CSSの関連部分
<style type="text/css">
/* 文字盤 */
.clockFace {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 時計の目盛り(時刻) */
.mark_hour {
position: absolute;
width: 2px; /* 目盛りの太さ */
--mark_minute_long : calc(var(--clocksize) * 0.05);
height: var(--mark_minute_long); /* 目盛りの長さ */
background: #000;
transform-origin: right;
/* clockFaceで盤面の中心に設置 */
/* 時計盤幅の半分ずらして、目盛りの長さの半分ずらす */
transform: rotate(calc(30deg * var(--j))) translate(0, calc(var(--clocksize) * -0.5 + var(--mark_minute_long)/2));
}
/* 時計の目盛り(分) */
.mark_minute {
position: absolute;
width: 1px; /* 目盛りの太さ */
--mark_hour_long : calc(var(--clocksize) * 0.03);
height: var(--mark_hour_long); /* 目盛りの長さ */
background: #000;
transform-origin: right;
/* clockFaceで盤面の中心に設置 */
/* 時計盤幅の半分ずらして、目盛りの長さの半分ずらす */
transform: rotate(calc(6deg * var(--j))) translate(0, calc(var(--clocksize) * -0.5 + var(--mark_hour_long)/2));
}
/* 数字を置く領域 */
.number_area {
position: absolute;
font-size: calc(var(--clocksize)* 0.08);
width: calc(var(--clocksize)* 0.08);
height: calc(var(--clocksize)* 0.08);
text-align: center;
/*
数字を置く領域を盤面(clockFace)の中心に設置
そして、translate(x軸方向の移動距離, y軸方向の移動距離) で
盤面幅の半分ずらす。 calc(var(--clocksize) * -0.5
図面を見ながら微調整。今回は0.78倍
角度によって位置が変わるので、フォントサイズを元に微調整(var(--i) * calc(var(--clocksize)* 0.08) * 0.01)
*/
transform: rotate(calc(30deg * var(--i))) translate(0, calc(var(--clocksize) * -0.5 * 0.78 - var(--i) * (var(--clocksize)* 0.08) * 0.01) );
transform-origin: center;
}
/* 文字 */
.number {
font-size: calc(var(--clocksize)* 0.08);
width: calc(var(--clocksize)* 0.08);
height: calc(var(--clocksize)* 0.08);
/* 親のdivが傾いているので、文字の角度を戻す */
transform: rotate(calc(-30deg * var(--i)));
}
</style>
●HTMLの関連部分
<div class="clockFace"></div>
●JavaScriptの関連部分
<script>
//文字盤作成
function drawClockFace() {
const clockFace = document.querySelector(".clockFace");
//目盛り作成
let mark_hour;
let mark_minute;
for(let n = 0; n <= 59; n++) {
if ( n % 5 == 0) {
//時刻の目盛り作成
mark_hour = document.createElement('div');
mark_hour.className = 'mark_hour';
mark_hour.textContent = '';
mark_hour.style.cssText = '--j:' + n + ';'; //cssの変数定義
// 親要素の末尾に追加する
clockFace.appendChild(mark_hour);
} else {
//分の目盛り作成
mark_minute = document.createElement('div');
mark_minute.className = 'mark_minute';
mark_minute.textContent = '';
mark_minute.style.cssText = '--j:' + n + ';'; //cssの変数定義
// 親要素の末尾に追加する
clockFace.appendChild(mark_minute);
}
}
//数字作成
let number;
for(let m = 1; m <= 12; m++) {
//数字設置領域作成
number_area = document.createElement('div');
number_area.className = 'number_area';
number_area.style.cssText = '--i:' + m + ';'; //cssの変数定義
// 親要素の末尾に追加する
clockFace.appendChild(number_area);
//数字設置
number = document.createElement('div');
number.className = 'number';
number.textContent = m;
number.style.cssText = '--i:' + m + ';'; //cssの変数定義
//数字設置領域に数字を置
number_area.appendChild(number);
}
}
drawClockFace();
</script>
日付表示
日付表示位置はビューポート(vw)を元に計算していますが、場所を決める値は雰囲気で決めています。
JavaScriptで1秒ごとに日時を取得しています。
この部分です。
function updateClock() {
// 日時取得
const now = new Date();
}
setInterval(updateClock1000);
updateClock();
月と日付は1桁の場合は十の位に0を補完しています。これは月や日の数値の上の位に0を追加して下二桁を取得しています。JavaScriptのこの部分です。
${("0" + month).slice(-2)}月${("0" + date).slice(-2)}日
●CSSの関連部分
<style>
/* 日付表示 */
.dateArea {
position: absolute;
top: 30%; /* これは画面上から下方向にずらす距離 */
width: var(--clocksize);
font-size: calc(var(--clocksize)* 0.04);
text-align: center;
}
/* 午前午後表示 */
.branchAmPm {
position: absolute;
top: 35%; /* これは画面上から下方向にずらす距離 */
width: var(--clocksize);
font-size: calc(var(--clocksize)* 0.04);
text-align: center;
}
</style>
●HTMLの関連部分
<div class="branchAmPm"></div>
<div class="dateArea"></div>
●JavaScriptの関連部分
<script>
function updateClock() {
const dayArr = ["日", "月", "火", "水", "木", "金", "土"];
// 日時取得
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const date = now.getDate();
const day = now.getDay();
// 日付表示
const dateArea = document.querySelector(".dateArea");
let dateText = `${year}年${("0" + month).slice(-2)}月${("0" + date).slice(-2)}日(${dayArr[day]})`;
dateArea.textContent = dateText;
// 午前午後表示
const branchAmPm = document.querySelector(".branchAmPm");
if(hours >= 12) {
branchAmPm.textContent = "午後";
} else {
branchAmPm.textContent = "午前";
}
}
setInterval(updateClock, 1000);
updateClock();
</script>
時計の針を設置
時計の針は、時分秒ごとにHTMLのdivタグを作り、hour、minite、secondクラスを付けています。さらにすべての針にhandクラスを付けています。
handクラスで針の描画の様子を、他のクラスで針の形状を決めています。
JavaScriptのこの部分で、各針の角度を計算しています。角度に90度を加えているのは、時計の0時がてっぺんちょにあるからです。
const secondDegree = ((seconds / 60) * 360) + 90;
const minuteDegree = ((minutes / 60) * 360) + ((seconds/60)*6) + 90;
const hourDegree = ((hours / 12) * 360) + ((minutes/60)*30) + 90;
次のところで、CSSで針に角度を付けています。
secondHand.style.transform = `rotate(${secondDegree}deg)`;
minuteHand.style.transform = `rotate(${minuteDegree}deg)`;
hourHand.style.transform = `rotate(${hourDegree}deg)`;
これでアナログ時計は完成です
●CSSの関連部分
<style>
/* 時計の針共通 */
.hand {
position: absolute;
top: 50%; /* これは親要素 .clock の高さの50%下から描画 */
/* 針の右端を画面の中心に置く */
right: 50%;
/* 変形させる要素の中心点 右端を回転の中心にする */
transform-origin: right;
/* transform:変形後の表示効果 */
/*
ease-in 開始時は緩やかに、終了時は早く変化
ease-out ease-inとは逆に開始時は早く終了時は緩やかに
ease-in-out 開始時と終了時の変化をeaseより緩やかに
*/
transition: transform 0.0s;
}
/* 短針 */
.hour {
height: 15px; /* 針の太さ */
width: 35%; /* 針の長さ */
background: #000;
}
/* 長針 */
.minute {
height: 8px; /* 針の太さ */
width: 40%; /* 針の長さ */
background: #000;
}
/* 秒針 */
.second {
height: 1px; /* 針の太さ */
width: 45%; /* 針の長さ */
background: #000;
}
<style>
●HTMLの関連部分
<div class="hand hour" id="hour"></div>
<div class="hand minute" id="minute"></div>
<div class="hand second" id="second"></div>
●JavaScriptの関連部分
<script>
function updateClock() {
const secondHand = document.querySelector('#second');
const minuteHand = document.querySelector('#minute');
const hourHand = document.querySelector('#hour');
const secondDegree = ((seconds / 60) * 360) + 90;
const minuteDegree = ((minutes / 60) * 360) + ((seconds/60)*6) + 90;
const hourDegree = ((hours / 12) * 360) + ((minutes/60)*30) + 90;
// 時計の針を回す
secondHand.style.transform = `rotate(${secondDegree}deg)`;
minuteHand.style.transform = `rotate(${minuteDegree}deg)`;
hourHand.style.transform = `rotate(${hourDegree}deg)`;
// デジタル時計
const digital_clock = document.querySelector(".digital_clock");
const hours_digital = hours;
const minutes_digital = minutes;
const seconds_digital = seconds;
digital_clock.textContent = ( '0' + hours_digital ).slice( -2 ) + ":" + ( '0' + minutes ).slice( -2 ) + ":" + ( '0' + seconds ).slice( -2 );
}
setInterval(updateClock, 1000);
updateClock();
}
setInterval(updateClock, 1000);
updateClock();
</script>
おまけでデジタル時計も追加
デジタル時計の方が、正確な時刻が分かるかなと思い、おまけでデジタル時計を付け加えました。延びたり縮んだりするアナデジ時計の完成です。
●CSSの関連部分
<style>
/* デジタル時計 */
.digital_clock {
position: absolute;
top: calc(var(--clocksize) * 1.1); /* これは画面上から下方向にずらす距離 */
width: var(--clocksize);
font-size: calc(var(--clocksize)* 0.15);
text-align: center;
}
<style>
●HTMLの関連部分
<div class="digital_clock"></div>
●JavaScriptの関連部分
<script>
function updateClock() {
const seconds = now.getSeconds();
const minutes = now.getMinutes();
const hours = now.getHours();
// デジタル時計
const digital_clock = document.querySelector(".digital_clock");
const hours_digital = hours;
const minutes_digital = minutes;
const seconds_digital = seconds;
digital_clock.textContent = ( '0' + hours_digital ).slice( -2 ) + ":" + ( '0' + minutes ).slice( -2 ) + ":" + ( '0' + seconds ).slice( -2 );
}
setInterval(updateClock, 1000);
updateClock();
}
setInterval(updateClock, 1000);
updateClock();
</script>
Microsoft Copilotの支援
作るに当たってMicrosoft Copilotさんに手伝ってもらいました。JavaScriptからCSSに変数で値を渡すことができるのは、Copilotさんに教えてもらいました。
大変助かったのですが、いくつか困った点がありました。Copilotさんって天才なのでいろんなコードを示してくれます。でもコメントが少ないので、知識が浅い身に取ってはコードが理解できないことがありました。
Copilotさんって力業が好きそうです。汎用的でないご提案もあり、アイデアはいただくとしてコードを書き換えることがありました。すごかったのは、目盛りの描画の提案内容です。
分秒の目盛りは60個あるので、60回同じようなコードを書きなさいと。こんなのforループを使うのが普通でしょう。
<div class="marks">
<div class="mark" style="--j:1;"></div>
<div class="mark" style="--j:2;"></div>
<!-- Add more marks up to 60 -->
<div class="mark" style="--j:60;"></div>
</div>
作ったきっかけ
とあるイベントがあって、会場に社内の会議室を借りました。前日に会場に行って、机と椅子を動かして、使わない什器を壁際に寄せて準備が完了しました。
一段落して部屋を眺めていると、時計が無いことに気づきました。このイベントは時間に厳しく、参加者は開始、終了時刻が分からないと困るのです。全員が見える時計を掛けるところもありません😰。
そうだ、ディスプレーは使わないので、そこに大きな時計を映せば良いじゃんと考えたのでした😍。
本番での利用
フリーソフトを探してましたが、希望にマッチしたソフトが見当たりませんでした。本番は明日だ。どうしよう😥。でもなんとか希望に近いアナログ時計を発見しました。それはHTMLで作っていました。
こちらです。
プログラーニングさんのページ
こちらのコードを参考に改造しました。JavaScriptでcanvasに時計を描くコードです。すっご~くマメなコードです。ただ希望のサイズと違うので、安直にcanvasサイズを変更。さらにデジタル時計を追加して、イベントを乗り切りました。
const canvas = document.getElementById("clock");
const ctx = canvas.getContext('2d');
// canvasを拡大
ctx.scale(2, 2);
でもこのcanvasサイズの倍率を変える改造って力業で、ダサいと思いました。しかもコードの中に即値を入れています。これもダサいと思った点です。
アナログ時計の大改造
このアナログ時計、将来また出番があるかもしれない。という訳で、一念発起して改造を始めました。そこで時計のすべての部品のサイズを相対的に変えるには、他の人はどうやっているのか検索して調べていると、CSSでアナログ時計を作って紹介しているwebページを発見したのです。さらに調べていると、CSSって変数使えるじゃないですか。しかもJavaScriptから値をCSSに渡すことができます。transform?何これ、使ったことがありません。など恥ずかしながら、CSSの知らない機能を発見しました。研修の先生失格です😭。
このため、CSSメインのアナログ時計作りに励んだのでした。
最後に
スマホで開いてみて見て気づいたのですが、なんとスマホ時計になっています。しかも通信が不要なので、パケットは使いません。エコな時計ができあがりました。想定外の使い方を発見しました。
でも、いつ使おう?