JavaScriptの復習ついでに垂線を求めて描画してみる
( 個人技術ブログネタが無かったからすっかり空いてしまった…… )
最近は画像処理アレコレしているが、その中で座標の点を操作する処理をよく作ってる。
内容としては高校までの数学レベルなんだけど、具体的にコードに落としながらだとイージーミスしたり、業務レベルで使うこと考えてきれいに書いたりと、結構奥深い。
ここ最近作った中で、比較的他でも使いまわせそうなものについて、JavaScript として書き直してみる。
( 仕事では Python だった。久々に JavaScript の勉強もかねて。)
作成物
今回の作成物。
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 とかに慣れない。。復習かねてもう少し色々書きたい。