今回はVue.jsを使ってアナログ時計を作ってみたいと思います。
ちなみに完成図は以下のような感じです。
HTMLとCSSでアナログ時計を組む
<div id="app" class="watch-body">
<!-- 時刻 -->
<div class="number">
<div class="letter-wrapper"><span>12</span></div>
<div class="letter-wrapper"><span>1</span></div>
<div class="letter-wrapper"><span>2</span></div>
<div class="letter-wrapper"><span>3</span></div>
<div class="letter-wrapper"><span>4</span></div>
<div class="letter-wrapper"><span>5</span></div>
<div class="letter-wrapper"><span>6</span></div>
<div class="letter-wrapper"><span>7</span></div>
<div class="letter-wrapper"><span>8</span></div>
<div class="letter-wrapper"><span>9</span></div>
<div class="letter-wrapper"><span>10</span></div>
<div class="letter-wrapper"><span>11</span></div>
</div>
<!-- 針 -->
<div class="hands">
<span class="dot"></span>
<span class="hour-hand-wrapper" :style="{ transform: 'rotate(' + calcHourHandAngle + 'deg)' }">
<span class="hour-hand"></span>
</span>
<span class="minute-hand-wrapper" :style="{ transform: 'rotate(' + calcMinuteHandAngle + 'deg)' }">
<span class="minute-hand"></span>
</span>
<span class="second-hand-wrapper" :style="{ transform: 'rotate(' + calcSecondHandAngle + 'deg)' }">
<span class="second-hand"></span>
</span>
</div>
</div>
* {
margin:0;
box-sizing:border-box;
font-family: 'Signika Negative', sans-serif;
}
html {
height: 100%;
}
body {
padding:50px;
height:100%;
display:flex;
justify-content:center;
align-items:center;
background:#444;
}
/* 時計本体 */
.watch-body {
border:10px solid #444;
border-radius:100%;
display:block;
position:relative;
height:300px;
aspect-ratio:1/1;
background:#fff;
}
/* 盤面文字 */
.watch-body .number {
position:absolute;
left:0;
top:0;
border-radius:100%;
width:100%;
height:100%;
}
.watch-body .number div.letter-wrapper {
position:absolute;
width:2em;
height:2em;
font-weight:bold;
display:inline-flex;
align-items:flex-start;
justify-content:center;
font-size:20px;
height:100%;
padding:10px 0;
top:0;
left:calc((100% - 2em) / 2);
}
.watch-body .number div.letter-wrapper:nth-of-type(2) {
transform:rotate(30deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(2) span {
transform:rotate(-30deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(3) {
transform:rotate(60deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(3) span {
transform:rotate(-60deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(4) {
transform:rotate(90deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(4) span {
transform:rotate(-90deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(5) {
transform:rotate(120deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(5) span {
transform:rotate(-120deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(6) {
transform:rotate(150deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(6) span {
transform:rotate(-150deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(7) {
transform:rotate(180deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(7) span {
transform:rotate(-180deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(8) {
transform:rotate(210deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(8) span {
transform:rotate(-210deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(9) {
transform:rotate(240deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(9) span {
transform:rotate(-240deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(10) {
transform:rotate(270deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(10) span {
transform:rotate(-270deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(11) {
transform:rotate(300deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(11) span {
transform:rotate(-300deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(12) {
transform:rotate(330deg);
}
.watch-body .number div.letter-wrapper:nth-of-type(12) span {
transform:rotate(-330deg);
}
/* 針 */
.watch-body .dot {
display: block;
width: 8px;
height: 8px;
background: #fff;
border-radius: 100%;
position: absolute;
left: calc((100% - 8px) / 2);
top: calc((100% - 8px) / 2);
z-index: 999;
}
.watch-body .hour-hand-wrapper {
position:absolute;
height:100%;
width:15px;
left:calc((100% - 15px) / 2);
top:0;
}
.watch-body .hour-hand-wrapper .hour-hand {
background: #444;
width: 100%;
height: 35%;
position: absolute;
left: 0;
top: 20%;
border-radius: 100px;
}
.watch-body .minute-hand-wrapper {
position: absolute;
height: 100%;
width: 10px;
left: calc((100% - 10px) / 2);
top: 0;
}
.watch-body .minute-hand-wrapper .minute-hand {
background: #444;
width: 100%;
height: 45%;
position: absolute;
left: 0;
top: 10%;
border-radius: 100px;
}
.watch-body .second-hand-wrapper {
position: absolute;
height: 100%;
width: 5px;
left: calc((100% - 5px) / 2);
top: 0;
}
.watch-body .second-hand-wrapper .second-hand {
background: #444;
width: 100%;
height: 45%;
position: absolute;
left: 0;
top: 10%;
border-radius: 100px;
}
上記のHTMLとCSSでアナログ時計を作ることができます。
文字盤のところは以下の画像のようなイメージでHTMLを組んで、CSSを当てています。
一文字ずつを赤枠のdivで囲んでさらに青枠のspanで囲んでいます。
文字盤の文字間隔は1文字あたり30度の傾度で設置されてるので、赤枠divに(30*n)degずつ角度をつけていきます。
ただ、そのままだと文字にも傾きがついてしまうので青枠spanに対しても赤枠divに設定したdegのマイナス値をdegとして設定します。
これにより文字自体に対する傾きを打ち消すことができます。
これを12個の文字全部設定します。
次に時針・分針・秒針についてです。
こちらも文字と考え方は同じで、それぞれ青枠を針本体としてCSSをあて、赤枠spanで角度をつけていきます。
処理フローを考える
プログラムを組んでいく前に軽く処理フローを考えてみたいと思います。
大まかな処理はこのような感じです。
ページ読み込みを処理スタートのトリガーとし、
1.現在時刻取得
2.取得した時刻を配列に格納
3.2で取得した「時」「分」「秒」を計算し、それぞれ対応する針にdeg値を適用する
という処理を1セットとします。
このセットを1秒ごとに実行するようにインターバル化することで時計を実装できます。
プログラムを組んでいく
const app = Vue.createApp({
data() {
return {
dateTime: {
'hours': '',
'minutes': '',
'seconds': ''
}
}
},
/* mounted */
mounted() {
let me = this
setInterval(function(){
me.getDateTime()
}, 1000)
},
/* methods */
methods: {
/* getDateTime */
getDateTime() {
let time = new Date()
this.dateTime.hours = (time.getHours() >= 12)? time.getHours() - 12 : time.getHours()
this.dateTime.minutes = time.getMinutes()
this.dateTime.seconds = time.getSeconds()
}
},
/* computed */
computed: {
// calcHourHandAngle
calcHourHandAngle() {
// 計算式 ⇒ (hours / 12 * 360) + (minutes / 60 * 30)
return (this.dateTime.hours / 12 * 360) + (this.dateTime.minutes / 60 * 30)
},
// calcMinuteHandAngle
calcMinuteHandAngle() {
// 計算式 ⇒ minutes / 60 * 360
return this.dateTime.minutes / 60 * 360
},
// calcSecondHandAngle
calcSecondHandAngle() {
// 計算式 ⇒ seconds / 60 * 360
return this.dateTime.seconds / 60 * 360
}
}
})
JSの全体像はこのような感じになっています。
一つ一つ解説していきます。
data() {
return {
dateTime: {
'hours': '',
'minutes': '',
'seconds': ''
}
}
}
配列(オブジェクト)の定義です。
処理の中で取得した時刻の情報をdateTimeへと格納します。
/* methods */
methods: {
/* getDateTime */
getDateTime() {
let time = new Date()
this.dateTime.hours = (time.getHours() >= 12)? time.getHours() - 12 : time.getHours()
this.dateTime.minutes = time.getMinutes()
this.dateTime.seconds = time.getSeconds()
}
},
今回methodsで定義するメソッドはgetDateTimeのみです。
ここでは、new Date()メソッドで現在時刻を取得し、dateTimeに格納しています。
hoursのみ12時間表記にしています。
この理由としては、時針へ適用するdeg値の計算をする際に計算方式をシンプルにするために12時間表記にしています。
/* computed */
computed: {
// calcHourHandAngle
calcHourHandAngle() {
// 計算式 ⇒ (hours / 12 * 360) + (minutes / 60 * 30)
return (this.dateTime.hours / 12 * 360) + (this.dateTime.minutes / 60 * 30)
},
// calcMinuteHandAngle
calcMinuteHandAngle() {
// 計算式 ⇒ minutes / 60 * 360
return this.dateTime.minutes / 60 * 360
},
// calcSecondHandAngle
calcSecondHandAngle() {
// 計算式 ⇒ seconds / 60 * 360
return this.dateTime.seconds / 60 * 360
}
}
次にcomputedで時針・分針・秒針に適用する角度を計算する処理を定義しています。
dateTimeから値を取得し、それぞれの計算式でdeg値を返します。
/* mounted */
mounted() {
let me = this
setInterval(function(){
me.getDateTime()
}, 1000)
},
これらの処理はマウント時に1秒ごとのインターバルとして実行します。
<!-- 針 -->
<div class="hands">
<span class="dot"></span>
<span class="hour-hand-wrapper" :style="{ transform: 'rotate(' + calcHourHandAngle + 'deg)' }">
<span class="hour-hand"></span>
</span>
<span class="minute-hand-wrapper" :style="{ transform: 'rotate(' + calcMinuteHandAngle + 'deg)' }">
<span class="minute-hand"></span>
</span>
<span class="second-hand-wrapper" :style="{ transform: 'rotate(' + calcSecondHandAngle + 'deg)' }">
<span class="second-hand"></span>
</span>
</div>
HTML側では、それぞれの針でcomputedによるdeg計算値を取得することで常時針を動かすことが可能になります。