可视化学习:利用向量计算点到线段的距离并展示
本文可配合本人錄制的視頻一起食用。
引言
最近我在學可視化的東西,借此來鞏固一下學習的內容,向量運算是計算機圖形學的基礎,這個例子就是向量的一種應用,是利用向量來計算點到線段的距離,這個例子中可視化的展示采用Canvas2D來實現。
說起向量,當時一看到這個詞,我是一種很模糊的記憶;這些是中學學的東西,感覺好像都還給老師了。然后又說起了向量的乘法,當看到點積、叉積這兩個詞,我才猛然想起點乘和叉乘;但整體上還是模模糊糊的,不太記得兩者具體的定義了;就找資料快速過了一遍吧。
因為本文中不涉及向量的基礎知識;如果有跟我一樣遺忘的小伙伴,可以找點視頻回憶一下,或者是找點資料看下。
題面
首先本次的例子中要獲取兩個值,一個是點到線段的距離,另一個是點到線段所在直線的距離。
假設存在一個線段AB,以及一個點C;則他們之前的位置可能有三種情況:
-
點C在線段AB左側
-
點C在線段AB的上方或下方
-
點C在線段AB的右側
在第一種和第三種情況下,點C到線段AB的距離為點C到點A或點B的距離,即向量AC或向量BC的長度。
在第二種情況下,點C到線段AB和到線段AB所在直線的距離是一樣的,這個時候,我們就可以利用向量的乘法來解決這個距離的計算。
這個例子給的思路是利用向量的乘法,因為向量叉乘的幾何意義就是平行四邊形的面積,已知底邊長度,也就是線段AB的長度,然后就可以得出點C到直線的距離;但因為要在頁面上展示出來,所以我們需要求得點D的坐標。
思路
一開始我想的有點復雜,想要去求AB所在直線的函數方程,從而計算出點C是在直線的上方還是下方,雖然向量的叉乘我記得不太多了,但我依舊還記得,如果向量AB旋轉到向量CD為順時針,則向量AB叉乘向量CD的值就為正,如果是逆時針,就為負。
接著再利用叉乘和點乘,去計算點D的x坐標和y坐標;這其實有點把事情搞復雜了,另外還需要去特殊處理CD和X軸平行以及Y軸平行的特殊情況。
然后我看了別人的提示才反應過來,我們只要充分地利用向量的乘法就可以了,而不需要去求什么直線的函數方程,當然這也就不用考慮什么特殊情況。
由上圖可知AD是AC在AB上的投影,然后我們知道投影可以通過點乘來求得,要求兩個向量的點乘,有兩種計算方式,一種是通過坐標來計算,另一種是通過向量的模和夾角來計算;分別對應以下兩個公式:
- AC · AB = AC.x * AB.x + AC.y * AB.y
- AC · AB = |AC| * |AB| * cosθ
因為已知點A、點B和點C的坐標,所以我們可以利用以上兩個公式計算點D的坐標。
具體實現
現在我們就來通過Canvas來實現以上效果。
HTML
首先我們在HTML中先放一個Canvas標簽。
<canvas width="512" height="512"></canvas>
CSS
然后寫一點簡單的CSS樣式。
canvas {
margin: 0;
width: 512px;
height: 512px;
border: 1px solid #eee;
}
JavaScript
最后我們來編寫最重要的JavaScript代碼。
這里預先定義了一個Vector2D的類用于表示二維向量。
/*
* 定義二維向量
* */
export default class Vector2D extends Array {
constructor(x = 1, y = 0) {
super(x, y);
}
get x() {
return this[0];
}
set x(value) {
this[0] = value;
}
get y() {
return this[1];
}
set y(value) {
this[1] = value;
}
// 獲取向量的長度
get len() {
// x、y的平方和的平方根
return Math.hypot(this.x, this.y);
}
// 獲取向量與X軸的夾角
get dir() {
// 向量與X軸的夾角
return Math.atan2(this.y, this.x);
}
// 復制向量
copy() {
return new Vector2D(this.x, this.y);
}
// 向量的加法
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
// 向量旋轉
rotate(rad) {
const c = Math.cos(rad),
s = Math.sin(rad);
const [x, y] = this;
this.x = x * c - y * s;
this.y = x * s + y * c;
return this;
}
scale(length) {
this.x *= length;
this.y *= length;
return this;
}
// 向量的點乘
dot(v) {
return this.x * v.x + this.y * v.y;
}
// 向量的叉乘
cross(v) {
return this.x * v.y - v.x * this.y;
}
reverse() {
return this.copy().scale(-1);
}
// 向量的減法
minus(v) {
return this.copy().add(v.reverse());
}
// 向量歸一化
normalize() {
return this.copy().scale(1 / this.len);
}
}
x和y分別是向量的坐標,len獲取的是向量的長度、利用了Math對象上的方法,dot和cross方法分別對應的就是向量的點乘和叉乘。
接著就來編寫功能代碼。
-
首先是獲取canvas2d的上下文,并完成坐標的轉換
let canvas = document.querySelector('canvas'), ctx = canvas.getContext('2d'); ctx.translate(canvas.width / 2, canvas.height / 2); ctx.scale(1, -1);因為畫布原始的坐標系是以左上角為原點,X軸向左,Y軸向下,這不符合我們在數學中常用的配置。
這里我們先通過translate方法把坐標挪到畫布中心,再通過scale方法將坐標系繞X軸翻轉;通過這樣的轉換,就可以按照我們在數學中常見的坐標系來操作了。
-
然后我們來初始化三個點,也就是之前說的點A、點B和點C。
坐標可以隨便寫,只要范圍在-256到256之間就可以。
我這里就簡單定義三個在X軸上的點,并維護在一個Map中,方便后續在canvas上顯示三個點的標識;后面會加一個事件監聽來更新點C的坐標。
let map = new Map(); let v0 = new Vector2D(0, 0), v1 = new Vector2D(100, 0), v2 = new Vector2D(-100, 0); map.set('C', v0); map.set('A', v1); map.set('B', v2); -
然后就可以開始繪制
這里我們定義一個draw函數,然后調用它。
draw(); function draw() {}-
首先,為了看上去更清晰,我們可以把坐標系繪制出來。
因為接下去繪制的直線比較多,這里我簡單封裝一個繪制直線的方法。
function drawLine(start, end, color) { ctx.beginPath(); ctx.save(); ctx.lineWidth = '4px'; ctx.strokeStyle = color; ctx.moveTo(...start); ctx.lineTo(...end); ctx.stroke(); ctx.restore(); ctx.closePath(); }然后我們來繪制坐標系。
drawAxis(); function drawAxis() { drawLine([-canvas.width / 2, 0], [canvas.width / 2, 0], "#333"); drawLine([0, canvas.height / 2], [0, -canvas.height / 2], "#333"); } -
接著我們把點繪制到畫布上
for(const p of map) { drawPoint(p[1], p[0]); } function drawPoint(v, name, color='#333') { ctx.beginPath(); ctx.save(); ctx.fillStyle = color; ctx.arc(v.x, v.y, 2, 0, Math.PI * 2); ctx.scale(1, -1); ctx.fillText(`${name}`, v.x, 16 - v.y); ctx.restore(); ctx.fill(); }這里我們想把點的標識通過fillText也繪制到畫布上,但由于之前坐標被繞X軸翻轉過一次,所以直接繪制表示會導致文本是倒過來的,所以我們這里臨時把坐標系翻轉回來,完成文本繪制后,再通過restore恢復回去。
-
現在我們把線段AB也繪制出來
drawBaseline(); function drawBaseline() { drawLine(map.get('A'), map.get('B'), "blue"); } -
最后就是最關鍵的一步,把點C到線段AB和直線的距離求出來并展示在canvas畫布上
d為點C到線段AB的距離,dLine為點C到直線的距離;
result存儲的是AC和AB的點乘結果;crossProduct存儲的是AC和AB的叉乘結果。
根據叉乘結果,我們就可以計算出dLine的值,也就是點C到直線的距離。
drawLines(); function drawLines() { let AC = map.get('C').minus(map.get('A')); let AB = map.get('B').minus(map.get('A')); let BC = map.get('C').minus(map.get('B')); let result = AC.dot(AB); let d, dLine; // distance let crossProduct = AC.cross(AB); dLine = Math.abs(crossProduct) / AB.len; let pd = getD(); map.set('D', pd); if (result < 0) { // 角CAB為鈍角 drawLine(map.get('A'), map.get('C'), 'red'); drawLine(map.get('C'), pd, 'green'); d = AC.len; } else if (result > Math.pow(AB.len, 2)) { // 角CBA為鈍角 drawLine(map.get('B'), map.get('C'), 'red'); drawLine(map.get('C'), pd, 'green'); d = BC.len; } else { d = dLine; drawLine(map.get('C'), pd, 'red'); } let text = `點C到線段AB的距離:${Math.floor(d)}, 點C到AB所在直線的距離為${Math.floor(dLine)}`; drawText(text); } function getD() { let AC = map.get('C').minus(map.get('A')); let AB = map.get('B').minus(map.get('A')); let A = map.get('A'); // 即:向量OA // 已知:AD為AC在AB上的投影 // AD = (AB / |AB|) * (AC·AB / |AB|) // = AB * (AC·AB / |AB|2) // D.x - A.x = AD.x, D.y - A.y = AD.y let AD = AB.scale(AC.dot(AB) / AB.len**2); let D = new Vector2D( AD.x + A.x, AD.y + A.y ); return D; }然后我們來計算點D的坐標:
已知:AD是AC在AB上的投影。
所以AD可以表示為這樣:
(AB / |AB|) * (AC·AB / |AB|)向量AB除以AB的模即代表和向量AB同一方向夾角的單位向量,單位向量可以簡單理解為長度為1的向量;AC和AB的點積除以AB的模結果等于AC的模乘以兩個向量夾角的余弦值。所以這兩個值相乘,就等于是向量AD。
通過調整上面的公式,我們可以得到AD =
AB * (AC·AB / |AB|2),因為A、B、C的坐標都已知,也就可以得到向量AD的坐標。然后我們又知道向量AD的坐標可以直接通過向量的減法得到,也就是:
- AD.x = D.x - A.x
- AD.y = D.y - A.y
所以我們就可以得到點D的坐標,即
(AD.x + A.x, AD.y + A.y)。接著我們根據AC和AB的點乘結果result,來繪制相應的直線。
-
當result為負數時,說明AC和AB夾角的余弦值大于90度
即∠CAB為鈍角,說明點C到線段AB的距離就是點C到點A的距離。
-
而當result大于AC長度的平方,也就是AC的模乘以余弦值大于AB的模,也就是說,AC在向量AB上的投影大于AB的長度
那么此時∠CBA是鈍角,點C到線段AB的距離就是點C到點B的距離。
-
當result為0時,說明兩個向量互相垂直
此時,點C在線段AB的上方或下方,點C到線段AB的距離就是點C到直線的距離。也就是我們前面求到的dLine的值。
最后我們將結果通過fillText方法繪制到屏幕上。
function drawText(distance) { ctx.beginPath(); ctx.save(); ctx.font = "16px serif"; ctx.scale(1, -1); ctx.fillText(`${distance}`, -250, 240); ctx.restore(); } -
最后我們加一個鼠標移動事件,動態地更新點C的坐標,以及點C到線段AB和直線的距離。
initEvents(); function initEvents() { canvas.addEventListener('mousemove', e => { const rect = canvas.getBoundingClientRect(); ctx.clearRect(-canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height); let x = e.pageX - rect.left - canvas.width / 2; let y = -(e.pageY - rect.top - canvas.height / 2); v0 = new Vector2D(x, y); map.set('C', v0); draw(); }); }
好啦,到這里為止一個簡單的距離展示就完成了;我們可以通過移動鼠標來查看最后的效果。
-
總結
以上是生活随笔為你收集整理的可视化学习:利用向量计算点到线段的距离并展示的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 歌曲《诗人漫步(翻自 蔡依林) 》歌词及
- 下一篇: 泰国True与中兴合作建设5G商用网络