雑食性雑感雑記

知識の整理場。ため込んだ知識をブログ記事として再構築します。

JavaScriptの復習ついでに垂線を求めて描画してみる

( 個人技術ブログネタが無かったからすっかり空いてしまった…… )

最近は画像処理アレコレしているが、その中で座標の点を操作する処理をよく作ってる。
内容としては高校までの数学レベルなんだけど、具体的にコードに落としながらだとイージーミスしたり、業務レベルで使うこと考えてきれいに書いたりと、結構奥深い。

ここ最近作った中で、比較的他でも使いまわせそうなものについて、JavaScript として書き直してみる。
( 仕事では Python だった。久々に JavaScript の勉強もかねて。)

作成物


Canvas not supported.

今回の作成物。
480 × 480 サイズの canvas の中でマウスクリックに反応し、■の点を打ちます。
・2点目を打点 ⇒ 2点を結ぶ直線を引く。
・3点目以上 ⇒ 前に書いた直線と垂直に交わる直線を引く。
動作を行います。

面白い絵柄になることを期待したけど、
よく考えたら初めに描画した直線の角度に依存して、あとは垂線しか引けないので、格子柄にしかならないのだった……

要素

線分を画面端まで伸ばす

傾き (= 変化量) と切片は中学校数学で出るやつですね。

// 傾き & 切片
slope = (y2 - y1) / (x2 - x1);
intercept = y1 - slope * x1;

例外処理 (傾き取れない場合) を考える必要あり。

// 傾きが無い or ゼロの場合
if (x2 - x1 == 0) { return {x1: x1, y1: 0, x2: x2, y2: HEIGHT}; }
if (y2 - y1 == 0) { return {x1: 0, y1: y1, x2: WIDTH, y2: y2}; }

後は画面端を考えれば OK。
左端の場合、x = 0 の場合を考えて、そのときの y が画面の上下端に行った場合を調整すればよい。

// 画面左端
left_x = 0; 
left_y = intercept;
if (left_y < 0) {
    // 画面端 (下)
    left_x = -intercept / slope;
    left_y = 0;
}
else if (left_y > HEIGHT) {
    // 画面端 (上)
    left_x = (HEIGHT - intercept) / slope;
    left_y = HEIGHT;
}

右端の場合も同じ。

コード :

////////////////////
//
// 2つの点で設定された線分を画面端まで伸ばす
//
function getExtendLine(x1, y1, x2, y2) 
{
    // 傾きが無い or ゼロの場合
    if (x2 - x1 == 0) { return {x1: x1, y1: 0, x2: x2, y2: HEIGHT}; }
    if (y2 - y1 == 0) { return {x1: 0, y1: y1, x2: WIDTH, y2: y2}; }

    // 計算用に座標上下反転
    y1 = HEIGHT - y1;
    y2 = HEIGHT - y2;

    // 傾き & 切片
    slope = (y2 - y1) / (x2 - x1);
    intercept = y1 - slope * x1;

    // 画面左端
    left_x = 0; 
    left_y = intercept;
    if (left_y < 0) {
        // 画面端 (下)
        left_x = -intercept / slope;
        left_y = 0;
    }
    else if (left_y > HEIGHT) {
        // 画面端 (上)
        left_x = (HEIGHT - intercept) / slope;
        left_y = HEIGHT;
    }
    
    // 画面右端
    right_x = WIDTH;
    right_y = slope * WIDTH + intercept;
    if (right_y < 0) {
        // 画面端 (下)
        right_x = -intercept / slope;
        right_y = 0;
    } else if (right_y > HEIGHT) {
        // 画面端 (上)
        right_x = (HEIGHT - intercept) / slope;
        right_y = HEIGHT;
    }

    // 座標戻す
    left_y = HEIGHT - left_y;
    right_y = HEIGHT - right_y;

    return {x1: left_x, y1: left_y, x2: right_x, y2: right_y};
}

垂線の座標を求める

垂線はベクトルを使うのが簡単なのですね。内積とか久しぶり。
触ると思い出しますが、やらないと忘れたまま。

長さ1の単位ベクトルを求め、内積を取ってそれだけ進めば OK。
詳しい解説はいっぱいそういう記事あるので丸投げ。。

コード :

////////////////////
//
// 線分に対して垂線の足を延ばす
//
function getPerpendicular(line, point) 
{
    x1 = line.x1; y1 = line.y1;
    x2 = line.x2; y2 = line.y2;
    px = point.x; py = point.y;

    // 線分Wの単位ベクトルUを求める
    W = {x: x2 - x1, y: y2 - y1};
    dist_w = Math.sqrt(Math.pow(W.x, 2) + Math.pow(W.y, 2));
    U = {x: W.x / dist_w, y: W.y / dist_w};

    // A ... point から線分の片方の端までのベクトル
    A = {x: px - x1, y: py - y1};

    // t ... A と U との内積
    t = A.x * U.x + A.y * U.y;

    // 線分の片方の端から t 倍だけ進んだ先の点が求める座標
    H = {x: x1 + t * U.x, y: y1 + t * U.y};

    return H;
}

まとめ

後は、クリック時処理として「2点以上打たれていたら」「1点以上打たれていたら」の処理を入れるだけ。

////////////////////
//
// onload
//
window.onload = function()
{
    const canvas = document.getElementById("canvas");
    if (canvas.getContext) {
        context = canvas.getContext("2d");

        // 幅・高さを取得
        WIDTH = canvas.width;
        HEIGHT = canvas.height;

        // 外側領域を結ぶ
        context.strokeRect(0, 0, canvas.width, canvas.height);
    }

    // Click event
    function onClick(e) {
        context.beginPath();

        // クリック点取得して描画
        const rect = e.target.getBoundingClientRect();
        x = e.clientX - rect.left;
        y = e.clientY - rect.top;
        context.fillRect(x - 5, y - 5, 10, 10);
        const point = {x: x, y: y};

        // 線の色
        context.strokeStyle = getColor();

        // 既に2点以上打たれていたら、前2点を用いて垂線を引く
        if (points.length >= 2) {

            // 前2点の直線
            const pre1 = points[points.length - 1];
            const pre2 = points[points.length - 2];
            const ext_line = getExtendLine(pre1.x, pre1.y, pre2.x, pre2.y);

            // 垂線を引き、point 追加
            const p_point = getPerpendicular(ext_line, point);
            points.push(p_point);

            // 垂線を端まで伸ばして線を引く
            const ext_p_line = getExtendLine(x, y, p_point.x, p_point.y);
            context.moveTo(ext_p_line.x1, ext_p_line.y1);
            context.lineTo(ext_p_line.x2, ext_p_line.y2);
        }

        // 1点だけ打たれていたら、直線を引く
        else if (points.length >= 1) {
            const pre = points[points.length - 1];

            // 直線を引く
            const ext_line = getExtendLine(x, y, pre.x, pre.y);
            context.moveTo(ext_line.x1, ext_line.y1);
            context.lineTo(ext_line.x2, ext_line.y2);
        }
        context.stroke();

        // Point 追加
        points.push(point)
    }
    canvas.addEventListener('click', onClick, false);
}


久しぶりに JavaScript 触りましたが手軽で面白いですね。
var ばかりで生きてきたので let とか const とかに慣れない。。復習かねてもう少し色々書きたい。

参考にしたもの

  • 配色

developer.mozilla.org

  • Canvas イベント操作

qiita.com

アイキャッチ画像