JSで、SVGオブジェクトを任意の閉ループ上で動かす、止める。


### はじめに
SVGは、いろいろなサイトで紹介されいるように、SVGの要素だけでスマートなアニメーションを含んだ描画ができます。より細かなアニメーション制御では、CSSも利用できます。
今回の記事では、SVGオブジェクトが、任意に作ったコース上を、動かしたり、スピードを変えたり、止めたりできるように、JavascriptでSVGオブジェクトを動的に制御する方法を説明します。

キーワード
SVGオブジェクト移動、閉ループ、セグメント化、時計回り

### 特徴
- マウスで、任意の多角形を作ることができます。
- 出来上がった多角形の頂点を通る滑らかなコースに変換します。コースの中に急なコーナーがある場合、多角形の頂点の移動ができます。
- 作ったコース（センターコース）に並走する内側コース、外側コースを設けることができます。
- ３つのコースを走るSVGオブジェクトは、オブジェクト毎に、動く、スピードを変える、止めるの制御ができます。走るSVGオブジェクトは円ではなく、走る方向が分かる三角形としました。

### デモとリソース
[moveSVGObject](https://github.com/osaka3t2/moveSVGObject?tab=readme-ov-file)

### 操作方法
- SVGエリア（四角枠）内で、マウスクリックで、ポイントを入力します。ポイントが１つのとき、その位置を示すドットが表示されます。ポイントが２つのとき、ドットとドットを結ぶ線が表示されます。3点以上になると、多角形が表示されます。
- Drawボタンを選択すると、指定した多角形が、滑らかな閉ループ（コース）が表示されます。
- Editボタンを選択すると、ポイントをドラックして、移動することができます。急なコーナーは、ポイントの位置編集で、滑らかなコーナーに変更することができます。
- 次にPerpendicularボタンを選択すると、コースが３つになります。
- 上のStartボタンを選択すると、３つSVGオブジェクトが動き出します。Startボタンはトグルボタンで、動き出すと、Stopボタンに変わります。
- Stopボタンを選択すると、SVGオブジェクトが止まります。
- スライダーバーを動かすと、SVGオブジェクトの速度が変化します。
- 左のStart/Stopボタンを選択すると、対応するSVGオブジェクトが動いたり、止まったりします。

### プログラムの解説
１．マウスで、滑らかなコースを作る
マウスで、複数のポイントを入力します。1点の入力では、その座標にドットを描画します。2点目では、点と点を結ぶ線が描画されます。3点目以上となると、点を結ぶ多角形が描画されます。多角形は、単純なループでも、結び目があるループを指定してOKです。
HTML画面と同様に、SVG画面の中でも、マウス座標を簡単に取得できます。
````
// ポイントの設定
let points = [];

$("#svgElement").mousedown(function(event) {
    let x = parseInt(event.pageX - $("#svgElement").position().left);
    let y = parseInt(event.pageY - $("#svgElement").position().top);
    points.push({x: x, y: y});

    $("#svgElement").empty();

    if (points.length > 2) {
        for (let i = 0; i < points.length - 1; i++) {
            lineD(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, 'green', 'l' + i, svgElement);
        }
        lineD(points[0].x, points[0].y, points[points.length - 1].x, points[points.length - 1].y, 'green', 'l' + (points.length - 1), svgElement);
        for (let i = 0; i < points.length; i++) {
            circleD(points[i].x, points[i].y, 3, 'red', 'c' + i, svgElement);
        }
    }
    else if (points.length == 2) {
        lineD(points[0].x, points[0].y, points[1].x, points[1].y, 'green', 'b', svgElement);
        circleD(points[0].x, points[0].y, 3, 'red', 'a', svgElement);
        circleD(points[1].x, points[1].y, 3, 'red', 'a', svgElement);
    }
    else if (points.length == 1) {
        circleD(points[0].x, points[0].y, 3, 'red', 'a', svgElement);
    }
});
````

２．多角形から滑らかな閉ループに変換する
ポイントを通る滑らかなpathは、[Smoothly close an SVG path with JavaScript](https://stackoverflow.com/questions/76702965/smoothly-close-an-svg-path-with-javascript)に従って求めました。このサイトの説明で分かるように、ベジェの終了点が、始点に回り込むことで、滑らかな閉ループとなるpathが取得できます。下記の簡易コードで、smoothingを0.15とすることで、ポイントに沿った曲線が得られました。
````
    function points2closedPath(points) {
        let smoothing = 0.15;
        let pathData = getCurvePathData(points, smoothing, true);

        let dd = pathDataToD(pathData, 1);
        return dd;
    }
````

３．pathをセグメント化（線分）する
前述で得られたpath上を、SVGオブジェクトをアニメーションすることができます。本記事では、SVGオブジェクトを動かすため、ベジェ曲線を細かセグメント化（線分）して、この線分上で、SVGオブジェクトを動きを制御します。セグメント化は、[SVG segmentize](https://www.npmjs.com/package/svg-segmentize)を利用しました。セグメント化では、セグメント長を指定し、前述のパス描画で求めたパス周囲長path.getTotalLength()をセグメント長で分割します。関数closedPath2Segmentsで得られたnn個のセグメントに分割しました。segLen=3とした場合、平均のセグメント長は3.017（4回測定）、最大値は3.363、最小値は2.629、標準偏差は0.140でした。多少の誤差が見られるものの、セグメント長単位に、セグメントが分割されたことを確認しました。
````
    function closedPath2Segments(pathLen, closedPath) {
        let nn = Math.floor((pathLen / segLen));
        let svg = `<svg id="svgElem" width="${W}" height="${H}"><path fill="none" stroke="black" d="${closedPath}"></path></svg>`;

        const simpleElement = (new DOMParser()).parseFromString(svg, "text/xml").documentElement;
        let segmented = Segmentize(simpleElement, {input: "svg", output: "svg", resolution: {path: nn}});

        let l = segmented.getElementsByTagName('line');
                ・
                ・
    }
````

４．ポイント -> 滑らかな閉ループ -> セグメント化の流れ
マウスアクションで取得したポイントからセグメント化までの流れは、下記のコードの通りです。
これらの処理で求めたq0に従い、SVGオブジェクトを移動させます。q0は、座標意外に、点pi-1と点piのなす角度、開始点からの距離を持ちます。
````
    let segLen = 3;//length of segment
    function points2curve2Segments(points, svgElement) {
        let closedPath = points2closedPath(points);//ポイントからベジェ曲線
        let pathLen = pathD(closedPath, 'red', 'p3', svgElement);//pathを描画し、周囲長を求める
        $('#p3').remove();

        let q0 = [];
        let dl = closedPath2Segments(pathLen, closedPath);//

        pathD(dl, 'green', 'p2', svgElement);//pathの表示

        return q0;
    }
````
５．ポイントの位置編集
本記事では、SVG画面上で、ポイント（多角形の頂点）の設定とポイントの位置編集を同時に行います。ポイントの設定は、SVG画面上で、マウスがダウンした位置を取得して、円などSVGオブジェクトを表示ます。ポイントの位置編集は、通常、該当のSVGオブジェクトをマウスで直接選択すれば、例えば$('circle')で、circleの位置などが取得できます。しかし、本件では、ポイント設定もあるため、簡易的に、位置編集モードでは、SVG画面でmousedownの位置と、ポイントの座標からポイントオブジェクトを求め、ポイントオブジェクトの位置編集を行いました。
````
$("#svgElement").mousedown(function(event) {
    if (mode == 'edit') {// ポイントの位置編集
        //クリックされた座標を保持します
        _clickX = event.pageX;
        _clickY = event.pageY;
        //クリックされた時の座標から要素を取得します
        for (let i = 0; i < points.length; i++) {
            let x = points[i].x + position.left;
            let y = points[i].y + position.top;
            if ((x - 10 <= _clickX && _clickX <= x + 10) && (y - 10 <= _clickY && _clickY <= y + 10)) {
                targetId = points[i].id;
                targetNum = i;
                break;
            }
        }
        if (!targetId) return;

        px = parseInt($('#' + targetId).attr('cx'));
        py = parseInt($('#' + targetId).attr('cy'));
    }
````

６．コースの内側、外側にもコースを設ける
SVGの機能で、オブジェクトを拡大・縮小して、相似なSVGオブジェクトを重ね表示で、見た目で、内側、外側のコースを作ることができます。この方法で作ったコースのコース長は同じとなります。本記事では、内側は短く、外側は長くなるようにするため、内側の閉ループ、外側の閉ループをそれぞれ求めます。
セグメント化を繋げた曲線では、マウスで取得したポイントが、曲線上にないことがあり、そのズレは、最大で3あることが分かりました。これらのポイントは、曲線上の最接近点に置き換えました。
このポイントp0について、曲線上の点pi-1、点pi+1を結んだ直線lの垂線を求めます。点iは、直線lの中点mを通る垂線から距離d離れたポイントd01、d02を求めます。ポイントd01、d02が閉ループの内側に位置するか、外側に位置するかは、閉ループが時計回りか、反時計回りかの判定（[多角形座標の右回り、左回り](https://www5d.biglobe.ne.jp/~noocyte/Programming/Geometry/PolygonMoment-jp.html)）、と、直線lと直線（m-d01）、直線lと直線（m-d02）の外積で、判定します。
求めた内側のポイント、外側のポイントそれぞれを滑らかな曲線に変換して、コースを設けます。多角形の頂点が少ないと、垂線から距離dを保つことが困難となるため、多角形の頂点は多い方が、垂線から距離dを保ったコースが得られます。


７．SVGオブジェクトを動かす
前述で求めたコース（q0）を、クラスMoveObjectに取り込み、SVGオブジェクトの移動、停止を行います。コース（q0）ではセグメント長毎の配列となっているため、この配列（座標、移動角）に従い進めば、SVGオブジェクトを滑らかに移動することができます。
具体的なSVGオブジェクトの移動は、SVGオブジェクトをラップするg要素のtransform（移動はtranlate、回転はrotate）属性で行い、移動速度（デフォルトは30msec）は、スライダーバーで変更できるようにしました。

８． 移動オブジェクトの作成とオブジェクトの移動
下記のようなクラスMoveObjectを定義し、メソッドmoveの中でオブジェクトを移動します。移動オブジェクトは、三角形とし、移動方向が分かるようにしました。移動は、単純にオブジェクトの位置（transform）をタイマーで変えています。
````
    let g = document.createElementNS("http://www.w3.org/2000/svg", "g");//移動するSVGオブジェクト
    $(g).attr({
        id: 'moveG'
    });
    let poly = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
    $(poly).attr({
        points: "0 0, 15 -3, 15 3"
    });
    g.append(poly)
    svgElement.appendChild(g);

    let q0 = points2curve2Segments(points, svgElement);
    moveObject = new MoveObject('moveG', points, q0);

MoveObjectクラスの定義
class MoveObject {
constructor(id, points, q0) {
    this.id = id;
    this.points = points;
    this.q0 = q0;
    this.mG = document.getElementById(id);
    this.moveStop = false;
    this.ig = 0;
    this.speed = 30;//デフォルト
}

async move() {
    if (this.moveStop) return;
    while (true) {
        if (this.moveStop) break;
        for (; this.ig < this.q0.length - 1; this.ig++) {
            if (this.moveStop) break;
            await this.delay(this.speed);
            let tt = 'translate(' + this.q0[this.ig].x + ',' + (this.q0[this.ig].y) + ') rotate(' + (this.q0[this.ig].degree - 180) + ')';
            this.mG.setAttribute('transform', tt);
        }
            if (this.ig >= this.q0.length - 1) this.ig = 0;
    }
}

delay(ms) {
	return new Promise((resolve,reject) => setTimeout(resolve, ms));
}
　　　　・
　　　　・    
}
````

#### まとめ
SVGオブジェクトは、SVG要素を使って、簡単にアニメーションすることができますが、Javascriptでは、SVGのg要素のtransform属性（移動、回転）で、より細かな制御ができました。一方で、閉ループの内側のループ、外側のループを作るでは、それなりのコード量が必要でした。内側のコースは短く、外側は長いコースを作ることができ、仮想のサーキットを体験することができました。


### 参考

- [Smoothly close an SVG path with JavaScript](https://stackoverflow.com/questions/76702965/smoothly-close-an-svg-path-with-javascript)
- [SVG segmentize](https://www.npmjs.com/package/svg-segmentize)
- [多角形座標の右回り、左回り](https://www5d.biglobe.ne.jp/~noocyte/Programming/Geometry/PolygonMoment-jp.html)