コンテンツへスキップ

Vue.jsを使ってアナログ時計を作ってみる

  • 未分類

今回はVue.jsを使ってアナログ時計を作ってみたいと思います。
ちなみに完成図は以下のような感じです。

https://neightbor.net/labo/watch/

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計算値を取得することで常時針を動かすことが可能になります。