【CSS】【JavaScript】CSSの三角関数を使って任意の2要素を線で結ぶ

 CSS の三角関数が主要ブラウザで実装されきる日が近づいてきたので、これの使用例として任意の2要素を直線で結ぶ方法を紹介します。

2023年はCSSで三角関数「sin(), cos(), tan()」が主要ブラウザのすべてで使用できるようになるぞ! | コリス

 コード例とデモが次です。CSSの atan2 を使っており、現在は Firefox と Safari でのみ期待通りに動きます。

クリックでソースコードを展開
    <!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>2点間の線</title>
    <style>
        h1 { font-size: 1.5em; }
        .container {
            display: grid;
            grid-template-columns: fit-content(100%) fit-content(100%);
            gap: 5em;
            position: relative;
        }

        .box {
            width: 10em;
            padding: 1em;
            display: flex;
            flex-direction: column;
            gap: 1em;
        }

        .box > div {
            width: 100%;
            height: 3em;
            cursor: pointer;
        }

        .box > div.active {
            border: solid 4px #888;
        }

        .a {
            background-color: #f44
        }

        .b {
            background-color: #0ff
        }

        .c {
            background-color: #0f0
        }

        .connect-line {
            height: 0.25em;
            background-color: #888;
            position: absolute;
            transform-origin: top left;
        }
    </style>
    <script>
    /**
     * ある要素を原点とした時の要素の座標
     */
    const getPosFromOriginEl = (origin, tgt) => {
        const originRect = origin.getBoundingClientRect();
        const originX = originRect.left;
        const originY = originRect.top;
        const tgtRect = tgt.getBoundingClientRect();
        return {
            left: tgtRect.left - originX,
            top: tgtRect.top - originY,
            right: tgtRect.right - originX,
            bottom: tgtRect.bottom - originY,
            center: {
                x: (tgtRect.right + tgtRect.left) / 2 - originX,
                y: (tgtRect.top + tgtRect.bottom) / 2 - originY,
            },
        };
    };

    /**
     * ある要素の右端の中点からある要素の左端の中点までの相対座標
     * @param from
     * @param to
     * @param origin
     * @return {{top: number, len: number, left: number, deg: number}}
     */
    const getLineA2B = (from, to, origin) => {
        origin = origin ?? document.body;
        const fromPos = getPosFromOriginEl(origin, from);
        const toPos = getPosFromOriginEl(origin, to);

        return {
            origin: {
                x: fromPos.right,
                y: fromPos.center.y,
            },
            size: {
                x: toPos.left - fromPos.right,
                y: toPos.center.y - fromPos.center.y
            },
        };
    };

    /**
     * 要素Aと要素Bをつなぐ線を描画する。
     * つなぐ線は position:absolute で描画し、つなぐ線の座標は原点を position:absolute による相対配置の元となる originEl の左上とする。
     */
    function renderConnectLine(aEl, bEl, originEl) {
        // 線の始点とX方向、Y方向への大きさを取得
        const a2bLine = getLineA2B(aEl, bEl, originEl);
        // ↑の線の情報に従って div 要素を生成
        const connectLineEl = document.createElement('div');
        // 始点を設定
        connectLineEl.style.left = `${a2bLine.origin.x}px`;
        connectLineEl.style.top = `${a2bLine.origin.y}px`;
        // 長さを設定
        connectLineEl.style.width = `${Math.sqrt(a2bLine.size.x ** 2 + a2bLine.size.y ** 2)}px`;
        // 線の向きを三角関数を使って設定
        connectLineEl.style.transform = `rotate(atan2(${a2bLine.size.y}, ${a2bLine.size.x}))`;
        // 常に共通のスタイルを CSS であてるためにクラス名を追加
        connectLineEl.classList.add('connect-line');
        // 原点の
        originEl.appendChild(connectLineEl);
    }

    /**
     * ↑の関数らを動かすイベントハンドラーを登録
     */
    document.addEventListener('DOMContentLoaded', () => {
        document.querySelectorAll('.box > div').forEach(itemEl => {
            itemEl.addEventListener('click', () => {
                // クリックしたらその箱の中の唯一の active 要素とする
                itemEl.parentNode.querySelectorAll('div').forEach(el => el.classList.remove('active'))
                itemEl.classList.add('active');
                // active な要素が二つ合ったらそれを繋ぐ
                const activeElList = document.querySelectorAll('.active');
                if (activeElList.length === 2) {
                    // 古い線を消す
                    document.querySelectorAll('.connect-line').forEach(el=>el.remove())
                    renderConnectLine(activeElList[0], activeElList[1], document.querySelector('.container'))
                }
            });
        })
    });
    </script>
</head>
<body>
<h1>クリックした左の箱とクリックした右の箱がつながります</h1>
<div class="container">
    <div class="box">
        <div class="a"></div>
        <div class="b"></div>
        <div class="c"></div>
    </div>
    <div class="box">
        <div class="a"></div>
        <div class="b"></div>
        <div class="c"></div>
    </div>
</div>

</body>
</html>
    

 CSSの三角関数を使っているのは次の部分です。

    /**
     * 要素Aと要素Bをつなぐ線を描画する。
     * つなぐ線は position:absolute で描画し、つなぐ線の座標は原点を position:absolute による相対配置の元となる originEl の左上とする。
     */
    function renderConnectLine(aEl, bEl, originEl) {
        // 線の始点とX方向、Y方向への大きさを取得
        const a2bLine = getLineA2B(aEl, bEl, originEl);
        // ↑の線の情報に従って div 要素を生成
        const connectLineEl = document.createElement('div');
        // 始点を設定
        connectLineEl.style.left = `${a2bLine.origin.x}px`;
        connectLineEl.style.top = `${a2bLine.origin.y}px`;
        // 長さを設定
        connectLineEl.style.width = `${Math.sqrt(a2bLine.size.x ** 2 + a2bLine.size.y ** 2)}px`;
        // 線の向きを三角関数を使って設定
        connectLineEl.style.transform = `rotate(atan2(${a2bLine.size.y}, ${a2bLine.size.x}))`;
        // 常に共通のスタイルを CSS であてるためにクラス名を追加
        connectLineEl.classList.add('connect-line');
        // 原点の
        originEl.appendChild(connectLineEl);
    }

 Y座標とX座標から三角関数で角度を出しています。中学、高校のころによくやったであろうアレです。正直このコードならば JavaScript でMath.atan2を使っても大して変わりませんが、rotate の様な角度を要求する機能を用いる際、三角関数は便利です。例の場合、長さが不定かつ JavaScript 内で決まるためあまり活用できている感がありません。明確に CSS 上で三角関数を扱う利点が出るのは単位の違う CSS 上で扱えるもの同士で角度を扱いたい時です。これは例えばrotate(atan2(3em, 5px))の様なものです。これでしたらフォントサイズが大きくなればなるほどいい感じに角度が急になります。現在、この単位が異なるもの、特にフォントサイズを用いるものは Firefox、Sfari でも動きませんがブラウザの実装が進めば有効になるはずです。もし有効になれば JavaScript 上で角度変更用のイベントリスナーを消してブラウザのレンダリングエンジンにその役目を任せられる場合が増えます。そうなるとコードが少なくて開発が楽、処理が少なくて web ページやアプリケーションの挙動が高速化といいことづくめです。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG