【JavaScript】 簡単!デジタル/アナログ時計をつくろう
更新日:2024/02/27
JavaScriptの勉強をしていると、一度は作ってみようと思うのがアナログ時計ですね。
僕も作ってみました。
今回はデジタル時計の作り方も、あわせて紹介します。
デジタル時計をつくってみる
デジタル時計はとても簡単です。
- setInterval()で1000ミリ秒(1秒)毎に、処理を呼び出す。
- 呼び出された処理で、現在時刻を表示する。
とても簡単です。
では実際にデジタル時計をつくってみます。
HTML
<div id ="date"></div>
<div id ="clock"></div>
JavaScript
(()=>{
// ゼロ埋めして2桁の数値にする
const zero = n => (n < 10 ) ? "0" + n.toString() : n.toString();
// 日付の文字列化
const youbi = ["日","月","火","水","木","金","土"];
const getDateString = date =>
`${ date.getFullYear() }年 ${ zero(date.getMonth() + 1) }月 ${ zero(date.getDate()) }日 ${ youbi[date.getDay()] }曜日`;
// 時間の文字列化
const getHourString = date =>
`${ zero(date.getHours()) }: ${ zero(date.getMinutes()) }: ${ zero(date.getSeconds()) }`;
// DOMの構築を待つ
window.addEventListener('DOMContentLoaded',()=> {
// 日時を表示するDOM要素を取得
const dateDiv = document.getElementById("date");
const clockDiv = document.getElementById("clock");
// 現在の日
let nowDate = null;
// 1秒周期のタイマーセット
setInterval( ()=>{
// 現在の日時を取得
const now = new Date();
// 日付が変わったら日付を再表示
if( nowDate !== now.getDate() ) {
nowDate = now.getDate();
dateDiv.innerText = getDateString(now);
}
// 時間を再表示
clockDiv.innerText = getHourString(now);
},1000);
});
})();
Date(日付)オブジェクトと日時の取得は、次のページをみてください。
参考記事:【JavaScript】 日時と時刻を取得して表示してみる
参考記事:【JavaScript】 Dateオブジェクトから和暦を取得する
このコードでは、数値にゼロを付加して2桁にしています。
その際、10未満かどうか判断して処理をしていますが、次の記事のような方法もあります。
参考記事:【JavaScript】 ゼロやスペースで埋めして桁揃えする
アナログ時計をつくってみる
次はアナログ時計をつくってみます。
針や文字などをcanvas要素を使って描画すると方法もありますが、この記事ではdiv要素のみで作ってみます。
参考記事:【JavaScript】 Canvasに描画してアナログ時計をつくろう!
文字盤をつくる
まずは土台となる文字盤を、次のようなイメージでつくります。
htmlは、divタグ一つだけ。
あとはJavaScriptで作成します。
HTML
<div id="analog">
</div>
CSSで高さを決めておきます。
可変にすると、高さを算出できません。
CSS
#analog{
height:400px;
width:100%;
position: relative;
}
position: relativeは、追加していく要素の基準座標をDIVタグの左上にするために必要です。
外(円)をつくる。
最初にアナログ時計の外周(円)をつくります。
JavaScript
(()=>{
const addDiv = (parentDiv,className,callBack = null ) => {
const t = document.createElement("div");
t.classList.add(className);
if( callBack && typeof callBack === "function") callBack(t);
parentDiv.appendChild(t);
return t;
}
const createFace = () =>{
const analog = document.getElementById("analog");
let vp = [analog.clientWidth,analog.clientHeight];
let chokei = Math.min(...vp);
const analogFace = addDiv(analog,"analog-face",( t )=>{
[t.style.height,t.style.width] = [chokei+"px",chokei+"px"];
[t.style.top,t.style.left] = [(vp[1]-chokei) / 2 + "px",(vp[0]-chokei) / 2 + "px"];
});
};
window.addEventListener("DOMContentLoaded", () => {
createFace();
});
})();
addDivは、指定した要素内にクラスを付与したdiv要素を追加しています。
これは今後も共通関数として使用します。
createFaceは、文字盤を作成する関数です。
- #analogのDOM要素を取得
- #analog内に時計の外周となる「analog-face」クラスを持つDIVタグを作成
- #analogの高さと幅のどちらか短い方を直径とする
- 直径を「analog-face」の高さと幅にセット
- 「analog-face」の位置を#analogの中心にくるようにtopとleftで調整
[ ] = [ ]という書式は、分割代入です。
何それ?という方は、こちら↓
【JavaScript】 分割代入はどこが便利なのか
「analog-face」を円にするのは、次のようにcssでおこなっています。
CSS
#analog .analog-face{
border: 3px solid black;
background-color:white ;
border-radius:50%;
position: absolute;
}
#analog div{
box-sizing: border-box;
}
border-radius:50%とすることで、円になります。
#analog-wrap divのbox-sizing: border-boxは、枠線を幅と高さに含めています。
最後のwindow.addEventListenerは、DOM要素が構築されて操作ができるまでまっています。
これを忘れると、何も表示されません。
このコードを実行すると、次のようにブラウザに表示されます。
外周に目盛りをつくる
次に外周に60個の目盛りをつくります。
JavaScript
const createFace = () =>{
// 外(円)をつくるのコードに追加
const r60 = 360 / 60;
const originX = analogFace.clientWidth / 2;
for( let i = 0 ; i < 60 ; i ++){
const deg = i * r60;
addDiv( analogFace , i % 5 ===0 ? "analog-line1" : "analog-line2",
( t )=>{
//t.style.left = leftPos + "pt";
if( i > 0 ){
t.style.transformOrigin = `${originX}px center`;
t.style.transform=`rotate(${deg}deg)`;
}
});
}
};
円の中心を基準にして、目盛り要素を回転しています。
中心点の算出は、これまで使用してきた半径(style.width)はズレるので、clientWidthを元に計算しています。
目盛りの初期位置はCSSで設定しています。
JavaScript
.analog-line1,.analog-line2{
position: absolute;
left:0;
z-index: 1;
}
.analog-line1{
width:5px;
height: 3px;
background: black;
top:calc(50% - 2px);
}
.analog-line2{
width:3px;
height: 2px;
background: black;
top:calc(50% - 1px);
}
実行すると次のようになります。
文字(時刻)を表示する
次は文字盤の時刻を表示します。
目盛りのように回転させると文字ごと回転してしまうので、別の方法をとります。
JavaScript
const createFace = () =>{
// これまでのコードに追加
const r12 = 360 / 12;
const hankei = originX;
const moziPos = hankei -30 ;
const MathPi = Math.PI / 180;
for( let i = 0 ; i < 12 ; i ++){
const deg = i * r12;
addDiv( analogFace ,"analog-text",
( t ) =>{
const mojiX = hankei + moziPos * Math.sin( deg * MathPi );
const mojiY = hankei - moziPos * Math.cos( deg * MathPi );
[t.style.top,t.style.left] = [mojiY + "px",mojiX + "px"];
t.innerText = i === 0 ? "12" : i.toString();
});
}
};
中心点から一定の距離にある点を、回転した座標を計算します。
今回は時計なので、一番上をゼロとして時計回りに回転させます。
計算式は、次のようになります。
cx = x + 距離 × sinΘ
cy = y - 距離 × cosΘ
Θ = 角度 × π ÷ 180
この式で得た座標は、文字の左上の座標となり見た目がよくありません。
そのためcssで文字をずらします。
CSS
.analog-text{
color:black;
font-size:2em;
position: absolute;
transform: translate(-50%, -50%);
z-index: 2;
}
transformで縦横半分だけ左上の移動させています。
実行すると次のようになります。
中心の点をつくる
最後に中心点をつくります。
JavaScript
const createFace = () =>{
// これまでのコードに追加
addDiv( analogFace , "analog-center" );
};
スクリプト上ではdiv要素を追加するだけです。
形状や位置はcssで設定します。
CSS
.analog-center,.analog-center:after{
position: absolute;
top:50%;
left:50%;
z-index: 15;
border-radius: 50%;
}
.analog-center{
background-color:black ;
height:21px;
width:21px;
transform: translate(-11px,-11px);
}
.analog-center:after{
content: "";
background-color:silver ;
height:17px;
width:17px;
transform: translate(-9px,-9px);
}
実行すると次のようになります。
秒針をつくって動かす
次は秒針をつくります。
考え方は外周に目盛りをつくったときと同じです。
今回は12を指している状態を初期値として、回転させます。
秒針を表示する関数は、分針・時針も利用できるようにオブジェクト化しておきます。
JavaScript:秒針を作成して回転させる
// 初回のみのアニメーション設定
const firstTransition = "transform 0.5s ease-out";
const handObj = function( className,{parentDiv:parentDiv
,LengthPer:LengthPer,handGapPer:handGapPer,divNum:divNum}){
// 針の作成
const hankei = parentDiv.clientHeight / 2;
const handLength = hankei * LengthPer / 100; // 針の長さ
const handGap = hankei * handGapPer / 100; // 針の飛び出している長さ
const elm = addDiv( parentDiv , className);
elm.style.height = (handLength + handGap) + "px";
[elm.style.top,elm.style.left] =
[ (hankei - handLength ) + "px", (hankei - elm.clientWidth/2) + "px"];
elm.style.transformOrigin = `center ${handLength}px `;
elm.style.transition = firstTransition;
this.rotateText = []; // rotate値をあらかじめ作成
const angle = 360 / divNum;
for( let i = 0 ; i < divNum ; i ++){
this.rotateText.push( `rotate(${ angle * i }deg)` );
}
this.elm = elm;
this.currentValue = null;
this.transitionFlg = true;
this.transitionCount = 0;
};
// 針の移動処理
handObj.prototype.moveHand=function( val ){
if( this.currentValue === val ) return;
if( this.transitionFlg && ++this.transitionCount > 1 ) {
// アニメーション効果削除
this.elm.style.transition=""; this.transitionFlg=false;
}
this.currentValue = val;
const r = this.angle * val ;
this.elm.style.transform = this.rotateText[ val ];
};
針は中心から少し飛び出る形状になっています。
そのことを考慮して、回転の中心を設定しています。
また0時の初期状態から、突然現在の時刻に画面が変わるのは見た目におもしろくないので、初回のみアニメーションしています。
アニメーションは処理が重いのと、時々逆回転するので、2回目以降はアニメーションしていません。
このコンストラクタからオブジェクトを作成して、setIntervalで一秒ごとに回転させます。
JavaScript:タイマーセット
const createFace = () =>{
// これまでのコードに追加
return analogFace;
};
window.addEventListener("DOMContentLoaded", () => {
const analogFace = createFace();
const secondHand = new handObj("analog-seconds",{
parentDiv:analogFace, // 親要素
LengthPer:85, // 針の長さ(パーセント)
handGapPer:20, // 飛び出す長さ(パーセント)
divNum:60 // 一周の分割数
});
setInterval(()=> {
const date = new Date();
secondHand.moveHand( date.getSeconds() );
},1000);
});
秒針のCSS
.analog-seconds{
background-color:red ;
width:5px;
position: absolute;
z-index: 10;
border-radius: 5px;
}
分針・時針をつくって動かす
分針・時針も秒針と同じ考え方でつくっていきます。
既にオブジェクト化してあるので、そのまま利用します。
JavaScript
window.addEventListener("DOMContentLoaded", () => {
const analogFace = createFace();
// 秒針
const secondHand = new handObj("analog-seconds",{
parentDiv:analogFace,
LengthPer:85,
handGapPer:20,
divNum:60
});
// 時針
const hourHand = new handObj("analog-hours",{
parentDiv:analogFace,
LengthPer:55,
handGapPer:10,
divNum:12 * 60
});
// 分針
const minuteHand = new handObj("analog-minutes",{
parentDiv:analogFace,
LengthPer:80,
handGapPer:10,
divNum:60
});
setInterval(()=> {
const date = new Date();
secondHand.moveHand(date.getSeconds());
hourHand.moveHand((date.getHours()%12) * 60 + date.getMinutes());
minuteHand.moveHand(date.getMinutes());
},1000);
});
時針は12分割にすると、少し問題がでてきます。
例えば次の時計は何時頃に見えるでしょうか?
『もうすぐ6時』に見えると思います。
しかし12分割で動いているとすると、6時55分になり、『もうすぐ7時』です。
大問題ですね。
そのため、分針と連動させるために、12×60分割しています。
時針と分針のCSS
.analog-hours{
background-color:black ;
width:10px;
position: absolute;
z-index: 8;
border-radius: 5px;
}
.analog-minutes{
background-color:black ;
width:10px;
position: absolute;
z-index: 9;
border-radius: 5px;
}
完成コード
html
<div id="analog">
</div>
CSS
#analog{
height:400px;
width:100%;
position: relative;
}
#analog .analog-face{
border: 3px solid black;
background-color:white ;
border-radius:50%;
position: absolute;
}
#analog div{
box-sizing: content-box;
}
.analog-line1,.analog-line2{
position: absolute;
left:0;
z-index: 1;
}
.analog-line1{
width:5px;
height: 3px;
background: black;
top:calc(50% - 2px);
}
.analog-line2{
width:3px;
height: 2px;
background: black;
top:calc(50% - 1px);
}
.analog-text{
color:black;
font-size:2em;
position: absolute;
transform: translate(-50%, -50%);
z-index: 2;
}
.analog-seconds{
background-color:red ;
width:5px;
position: absolute;
z-index: 10;
border-radius: 5px;
}
.analog-hours{
background-color:black ;
width:10px;
position: absolute;
z-index: 8;
border-radius: 5px;
}
.analog-minutes{
background-color:black ;
width:10px;
position: absolute;
z-index: 9;
border-radius: 5px;
}
.analog-center,.analog-center:after{
position: absolute;
top:50%;
left:50%;
z-index: 15;
border-radius: 50%;
}
.analog-center{
background-color:black ;
height:21px;
width:21px;
transform: translate(-11px,-11px);
}
.analog-center:after{
content: "";
background-color:silver ;
height:17px;
width:17px;
transform: translate(-9px,-9px);
}
JavaScript
(()=>{
const addDiv = (parentDiv,className,callBack = null ) => {
const t = document.createElement("div");
t.classList.add(className);
if( callBack && typeof callBack === "function") callBack(t);
parentDiv.appendChild(t);
return t;
}
const createFace = () =>{
const analog = document.getElementById("analog");
const vp = [analog.clientWidth,analog.clientHeight];
const chokei = Math.min(...vp);
const analogFace = addDiv(analog,"analog-face",( t )=>{
[t.style.height,t.style.width] = [chokei+"px",chokei+"px"];
[t.style.top,t.style.left] = [(vp[1]-chokei) / 2 + "px",(vp[0]-chokei) / 2 + "px"];
});
const r60 = 360 / 60;
const originX = analogFace.clientWidth/2;
for( let i = 0 ; i < 60 ; i ++){
const deg = i * r60;
addDiv( analogFace ,i % 5 ===0 ? "analog-line1" : "analog-line2",
( t )=>{
if( i > 0 ){
t.style.transformOrigin = `${originX}px center`;
t.style.transform=`rotate(${deg}deg)`;
}
});
}
const r12 = 360 / 12;
const hankei = originX;
const moziPos = hankei -30 ;
const MathPi = Math.PI / 180;
for( let i = 0 ; i < 12 ; i ++){
const deg = i * r12;
addDiv( analogFace ,"analog-text",
( t ) =>{
const mojiX = hankei + moziPos * Math.sin( deg * MathPi );
const mojiY = hankei - moziPos * Math.cos( deg * MathPi );
[t.style.top,t.style.left] = [mojiY + "px",mojiX + "px"];
t.innerText = i === 0 ? "12" : i.toString();
});
}
addDiv( analogFace , "analog-center" );
return analogFace;
};
const firstTransition = "transform 0.5s ease-out";
const handObj = function( className,{parentDiv:parentDiv
,LengthPer:LengthPer,handGapPer:handGapPer,divNum:divNum}){
const hankei = parentDiv.clientHeight / 2;
const handLength = hankei * LengthPer / 100;
const handGap = hankei * handGapPer / 100;
const elm = addDiv( parentDiv , className);
elm.style.height = (handLength + handGap) + "px";
[elm.style.top,elm.style.left] =
[ (hankei - handLength ) + "px", (hankei - elm.clientWidth/2) + "px"];
elm.style.transformOrigin = `center ${handLength}px `;
elm.style.transition=firstTransition;
this.rotateText = [];
const angle = 360 / divNum;
for( let i = 0 ; i < divNum ; i ++){
this.rotateText.push( `rotate(${ angle * i }deg)` );
}
this.elm = elm;
this.currentValue = null;
this.transitionFlg = true;
this.transitionCount = 0;
};
handObj.prototype.moveHand=function( val ){
if( this.currentValue === val ) return;
if( this.transitionFlg && ++this.transitionCount > 1 ) {
this.elm.style.transition=""; this.transitionFlg=false;
}
this.currentValue = val;
this.elm.style.transform = this.rotateText[val];
};
window.addEventListener("DOMContentLoaded", () => {
const analogFace = createFace();
const secondHand = new handObj("analog-seconds",{
parentDiv:analogFace,
LengthPer:85,
handGapPer:20,
divNum:60
});
const hourHand = new handObj("analog-hours",{
parentDiv:analogFace,
LengthPer:55,
handGapPer:10,
divNum:12 * 60
});
const minuteHand = new handObj("analog-minutes",{
parentDiv:analogFace,
LengthPer:80,
handGapPer:10,
divNum:60
});
setInterval(()=> {
const date = new Date();
secondHand.moveHand(date.getSeconds());
hourHand.moveHand((date.getHours()%12) * 60 + date.getMinutes());
minuteHand.moveHand(date.getMinutes());
},1000);
});
})();
アナログ時計:実行サンプル
更新日:2024/02/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。