【JavaScript】動くデジタル時計【three.js】

#はじめに
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/bcc3cb96-e7ca-c84e-1d18-c0ce99433bac.gif)
こんな感じのやつを電子工作で作ってみたくなって、イメージを沸かせるためにjavascriptで書いてみました。
自分用のメモ代わりの記事です。
ちなみに電子工作全くやったことないです。
three.jsを使っています。three.js初めて使いました。

#ソース
書きなぐっています。すいません。
あーコロン動かすの忘れた。まあいいや。おやすみなさい。

```html
<html>
<head>
<meta charset="utf-8" />
<style>
body {
    margin: 0;
}
</style>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
<script>
// ページの読み込みを待つ
$(e => {
    // サイズを指定
    const width = 960 / 2;
    const height = 540 / 2;

    // レンダラーを作成
    const renderer = new THREE.WebGLRenderer({
      canvas: document.querySelector('#myCanvas')
    });
    renderer.setSize(width, height);

    // シーンを作成
    const scene = new THREE.Scene();        

    // カメラを作成
    const camera = new THREE.PerspectiveCamera(
      45,
      width / height,
      1,
      10000
    );
    camera.position.set(0, 200, 1000);
    camera.lookAt(new THREE.Vector3(0, 0, 0));

    // カメラを作成
    //const camera = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2);

    // コンテナーを作成
    const container = new THREE.Object3D();
    scene.add(container);

    // マテリアルを作成
    const material = new THREE.MeshStandardMaterial({
      color: 0xcccccc,
      side: THREE.DoubleSide,
      flatShading: true,
    });

    // 平行光源を作成
    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(1, 1, 1);
    scene.add(directionalLight);
    // 環境光を作成
    const ambientLight = new THREE.AmbientLight(0x999999);
    scene.add(ambientLight);

    // アニメーション用の回転角度
    let rotXShift = 0.0025;
    let rotYShift = 0.01;

    const numDefs = [
        /* 0 */ [ 0, 1, 2, 4, 5, 6 ],
        /* 1 */ [ 2, 5 ],
        /* 2 */ [ 0, 2, 3, 4, 6 ],
        /* 3 */ [ 0, 2, 3, 5, 6 ],
        /* 4 */ [ 1, 2, 3, 5 ],
        /* 5 */ [ 0, 1, 3, 5, 6 ],
        /* 6 */ [ 0, 1, 3, 4, 5, 6 ],
        /* 7 */ [ 0, 2, 5 ],
        /* 8 */ [ 0, 1, 2, 3, 4, 5, 6 ],
        /* 9 */ [ 0, 1, 2, 3, 5, 6 ],
    ];

    const meshes = [];    

    createClock();

    const shifts = [];
    for(let i = 0; i < meshes.length; i += 1) {
        shifts.push([ 0, 0, 0, 0, 0, 0, 0 ]);
    }    

    tick();

    function updateShifts() {
        const time = getCurrentTime();
        initShifts();
        for(let i = 0; i < shifts.length; i += 1) {
            const shift = shifts[i];
            const t = parseInt(time[i]);
            const def = numDefs[t];
            def.forEach(n => {
                shift[n] = 1;
            });
        }
    }

    function initShifts() {
        for(let i = 0; i < shifts.length; i += 1) {
            for(let j = 0; j < 7; j += 1) {
                shifts[i][j] = -1;
            }
        }
    }

    function createClock() {
        const a = 20,
            b = 60, 
            c = 100,
            s = 5,      // 数字の要素の幅
            ss = 10,    // 数字間の幅
            cr = 20,    // :(コロン)の半径
            cbs = 60;   // コロンの上下のスペース
            cs = 20;    // コロンと数値のスペース

        // 時
        meshes[0] = createNumber(-a - ss * 0.5
             - (a + ss + a + s + a + b + a + s + a + cs + cr * 2 + cs + a + s + a + b + a + s), a, b, c, s, ss);
        meshes[1] = createNumber(a + ss * 0.5 + s + 2 * a + b + s
            - (a + ss + a + s + a + b + a + s + a + cs + cr * 2 + cs + a + s + a + b + a + s), a, b, c, s, ss);

        // 分
        meshes[2] = createNumber(-a - ss * 0.5, a, b, c, s, ss);
        meshes[3] = createNumber(a + ss * 0.5 + s + 2 * a + b + s, a, b, c, s, ss);  

        // 秒
        meshes[4] = createNumber(-a - ss * 0.5
             + a + ss + a + s + a + b + a + s + a + cs + cr * 2 + cs + a + s + a + b + a + s, a, b, c, s, ss);
        meshes[5] = createNumber(a + ss * 0.5 + s + 2 * a + b + s
            + a + ss + a + s + a + b + a + s + a + cs + cr * 2 + cs + a + s + a + b + a + s, a, b, c, s, ss);

        // 時と分のコロン
        createColon(-a - ss * 0.5, a, b, c, s, ss, cr, cbs, cs);  

        // 分と秒のコロン    
        createColon(-a - ss * 0.5
         + cr + cs + a + s + a + b + a + s + a + ss + a + s + a + b + a + s + a + cs + cr , a, b, c, s, ss, cr, cbs, cs); 
    }    

    //先頭ゼロ付加
    function padZero(num) {
        var result;
        if (num < 10) {
            result = "0" + num;
        } else {
            result = "" + num;
        }
        return result;
    }

    function getCurrentTime() {
        var now = new Date();
        return padZero(now.getHours()) + padZero(now.getMinutes()) + padZero(now.getSeconds());
    }

    // コロンを作成
    function createColon(shiftX, a, b, c, s, ss, cr, cbs, cs) {

        const ret = [];
        let geometry, mesh;
        geometry = new THREE.CylinderGeometry(cr, cr, c, 8);  
        mesh = new THREE.Mesh(geometry, material); 
        mesh.position.x = shiftX - 2 * a - b - s * 2 - a - cs - cr;
        mesh.position.y = cbs / 2;
        mesh.position.z = 0;
        mesh.rotation.x = Math.PI * 0.5;
        container.add(mesh);
        ret.push(mesh);

        geometry = new THREE.CylinderGeometry(cr, cr, c, 8);  
        mesh = new THREE.Mesh(geometry, material); 
        mesh.position.x = shiftX - 2 * a - b - s * 2 - a - cs - cr;
        mesh.position.y = -cbs / 2;
        mesh.position.z = 0;
        mesh.rotation.x = Math.PI * 0.5;
        container.add(mesh);
        ret.push(mesh);

        return ret;
    }

    // 数字を作成する
    function createNumber(shiftX, a, b, c, s, ss) {
        return [
            createHexGeometry({x: shiftX - b * 0.5 - a - s, y: 2 * a + b + s, z: 0 }, a, b, c),
            createHexGeometry({x: shiftX - 2 * a - b - s * 2 , y: (2 * a + b + s) * 0.5, z: 0 }, a, b, c, 1),
            createHexGeometry({x: shiftX, y: (2 * a + b + s) * 0.5, z: 0 }, a, b, c, 1),
            createHexGeometry({x: shiftX - b * 0.5 - a - s, y: 0, z: 0 }, a, b, c),
            createHexGeometry({x: shiftX - 2 * a - b - s * 2 , y: -(2 * a + b + s) * 0.5, z: 0 }, a, b, c, 1),
            createHexGeometry({x: shiftX, y: -(2 * a + b + s) * 0.5, z: 0 }, a, b, c, 1),
            createHexGeometry({x: shiftX - b * 0.5 - a - s, y: -(2 * a + b + s), z: 0 }, a, b, c),
        ];
    }

    // 六角柱を作成する
    function createHexGeometry(pos, a, b, c, rot) {
        // 六角柱の蓋を定義する
        const vertices = [
            // 上面の蓋
            new THREE.Vector3(a + b / 2,    0,      c / 2),     // #0
            new THREE.Vector3(b / 2,        a,      c / 2),     // #1
            new THREE.Vector3(-b / 2,        a,      c / 2),    // #2
            new THREE.Vector3(-a - b / 2,    0,      c / 2),    // #3
            new THREE.Vector3(-b / 2,        -a,      c / 2),   // #4
            new THREE.Vector3(b / 2,        -a,      c / 2),    // #5
            new THREE.Vector3(a + b / 2,    0,      -c / 2),    // #6
            new THREE.Vector3(b / 2,        a,      -c / 2),    // #7
            new THREE.Vector3(-b / 2,        a,     -c / 2),    // #8
            new THREE.Vector3(-a - b / 2,    0,      -c / 2),   // #9
            new THREE.Vector3(-b / 2,        -a,      -c / 2),  // #10
            new THREE.Vector3(b / 2,        -a,      -c / 2),   // #11
        ];

        const faces = [
            new THREE.Face3(0, 1, 5), // 上面
            new THREE.Face3(1, 2, 5), // 上面
            new THREE.Face3(2, 4, 5), // 上面
            new THREE.Face3(2, 3, 4), // 上面
            new THREE.Face3(6, 11, 7), // 下面
            new THREE.Face3(7, 11, 8), // 下面
            new THREE.Face3(8, 11, 10), // 下面
            new THREE.Face3(8, 10, 9), // 下面
            new THREE.Face3(0, 6, 7), // 側面 
            new THREE.Face3(7, 1, 0), // 側面
            new THREE.Face3(1, 7, 8), // 側面 
            new THREE.Face3(8, 2, 1), // 側面
            new THREE.Face3(2, 8, 9), // 側面 
            new THREE.Face3(9, 3, 2), // 側面
            new THREE.Face3(3, 9, 10), // 側面 
            new THREE.Face3(10, 4, 3), // 側面
            new THREE.Face3(4, 10, 11), // 側面 
            new THREE.Face3(11, 5, 4), // 側面
            new THREE.Face3(5, 11, 6), // 側面 
            new THREE.Face3(6, 0, 5), // 側面 
        ];

        const geometry = new THREE.Geometry();
        vertices.forEach(v => geometry.vertices.push(v));
        faces.forEach(f => geometry.faces.push(f));
        const mesh = new THREE.Mesh(geometry, material);
        mesh.position.x = pos.x;
        mesh.position.y = pos.y;
        mesh.position.z = pos.z;
        if(rot) {
            mesh.rotation.z = Math.PI * 0.5;
        }
        container.add(mesh);
        return mesh;
    }    

    // 毎フレーム時に実行されるループイベントです
    function tick() {

        // 移動情報取得
        updateShifts();

        // 数字を動かす
        for(let i = 0; i < meshes.length; i += 1) {
            const shift = shifts[i];
            for(j = 0; j < 7; j += 1) {
                const delta = shift[j];
                meshes[i][j].position.z += 5 * delta;
                if(meshes[i][j].position.z > 50) {
                    meshes[i][j].position.z = 50;
                } else if(meshes[i][j].position.z < -50) {
                    meshes[i][j].position.z = -50;
                }
            }
        }

        // コンテナを回転させる
        container.rotation.x += rotXShift;
        container.rotation.y += rotYShift;
        
        if(container.rotation.x >= Math.PI * 0.2
            || container.rotation.x <= -Math.PI * 0.2) {
            rotXShift *= -1;
        }
        if(container.rotation.y >= Math.PI * 0.2 || 
            container.rotation.y <= -Math.PI * 0.2) {
            rotYShift *= -1;
        }

        // レンダリング
        renderer.render(scene, camera);

        requestAnimationFrame(tick);
    }
});
</script>
</head>
<body>
    <canvas id="myCanvas"></canvas>
</body>
</html>

```