JS可視化學(xué)習(xí)向量計(jì)算點(diǎn)到線段的距離并展示
引言
本文可配合本人錄制的視頻一起食用。
最近我在學(xué)可視化的東西,借此來鞏固一下學(xué)習(xí)的內(nèi)容,向量運(yùn)算是計(jì)算機(jī)圖形學(xué)的基礎(chǔ),這個(gè)例子就是向量的一種應(yīng)用,是利用向量來計(jì)算點(diǎn)到線段的距離,這個(gè)例子中可視化的展示采用Canvas2D來實(shí)現(xiàn)。
說起向量,當(dāng)時(shí)一看到這個(gè)詞,我是一種很模糊的記憶;這些是中學(xué)學(xué)的東西,感覺好像都還給老師了。然后又說起了向量的乘法,當(dāng)看到點(diǎn)積、叉積這兩個(gè)詞,我才猛然想起點(diǎn)乘和叉乘;但整體上還是模模糊糊的,不太記得兩者具體的定義了;就找資料快速過了一遍吧。
因?yàn)楸疚闹胁簧婕跋蛄康幕A(chǔ)知識(shí);如果有跟我一樣遺忘的小伙伴,可以找點(diǎn)視頻回憶一下,或者是找點(diǎn)資料看下。
題面
首先本次的例子中要獲取兩個(gè)值,一個(gè)是點(diǎn)到線段的距離,另一個(gè)是點(diǎn)到線段所在直線的距離。
假設(shè)存在一個(gè)線段AB,以及一個(gè)點(diǎn)C;則他們之前的位置可能有三種情況:
點(diǎn)C在線段AB左側(cè)

點(diǎn)C在線段AB的上方或下方

點(diǎn)C在線段AB的右側(cè)

在第一種和第三種情況下,點(diǎn)C到線段AB的距離為點(diǎn)C到點(diǎn)A或點(diǎn)B的距離,即向量AC或向量BC的長(zhǎng)度。
在第二種情況下,點(diǎn)C到線段AB和到線段AB所在直線的距離是一樣的,這個(gè)時(shí)候,我們就可以利用向量的乘法來解決這個(gè)距離的計(jì)算。
這個(gè)例子給的思路是利用向量的乘法,因?yàn)橄蛄坎娉说膸缀我饬x就是平行四邊形的面積,已知底邊長(zhǎng)度,也就是線段AB的長(zhǎng)度,然后就可以得出點(diǎn)C到直線的距離;但因?yàn)橐陧?yè)面上展示出來,所以我們需要求得點(diǎn)D的坐標(biāo)。

思路
一開始我想的有點(diǎn)復(fù)雜,想要去求AB所在直線的函數(shù)方程,從而計(jì)算出點(diǎn)C是在直線的上方還是下方,雖然向量的叉乘我記得不太多了,但我依舊還記得,如果向量AB旋轉(zhuǎn)到向量CD為順時(shí)針,則向量AB叉乘向量CD的值就為正,如果是逆時(shí)針,就為負(fù)。
接著再利用叉乘和點(diǎn)乘,去計(jì)算點(diǎn)D的x坐標(biāo)和y坐標(biāo);這其實(shí)有點(diǎn)把事情搞復(fù)雜了,另外還需要去特殊處理CD和X軸平行以及Y軸平行的特殊情況。
然后我看了別人的提示才反應(yīng)過來,我們只要充分地利用向量的乘法就可以了,而不需要去求什么直線的函數(shù)方程,當(dāng)然這也就不用考慮什么特殊情況。

由上圖可知AD是AC在AB上的投影,然后我們知道投影可以通過點(diǎn)乘來求得,要求兩個(gè)向量的點(diǎn)乘,有兩種計(jì)算方式,一種是通過坐標(biāo)來計(jì)算,另一種是通過向量的模和夾角來計(jì)算;分別對(duì)應(yīng)以下兩個(gè)公式:
AC · AB = AC.x * AB.x + AC.y * AB.y
AC · AB = |AC| * |AB| * cosθ
因?yàn)橐阎c(diǎn)A、點(diǎn)B和點(diǎn)C的坐標(biāo),所以我們可以利用以上兩個(gè)公式計(jì)算點(diǎn)D的坐標(biāo)。
具體實(shí)現(xiàn)
現(xiàn)在我們就來通過Canvas來實(shí)現(xiàn)以上效果。
HTML
首先我們?cè)贖TML中先放一個(gè)Canvas標(biāo)簽。
<canvas width="512" height="512"></canvas>
CSS
然后寫一點(diǎn)簡(jiǎn)單的CSS樣式。
canvas {
margin: 0;
width: 512px;
height: 512px;
border: 1px solid #eee;
}JavaScript
最后我們來編寫最重要的JavaScript代碼。
這里預(yù)先定義了一個(gè)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;
}
// 獲取向量的長(zhǎng)度
get len() {
// x、y的平方和的平方根
return Math.hypot(this.x, this.y);
}
// 獲取向量與X軸的夾角
get dir() {
// 向量與X軸的夾角
return Math.atan2(this.y, this.x);
}
// 復(fù)制向量
copy() {
return new Vector2D(this.x, this.y);
}
// 向量的加法
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
// 向量旋轉(zhuǎn)
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;
}
// 向量的點(diǎn)乘
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分別是向量的坐標(biāo),len獲取的是向量的長(zhǎng)度、利用了Math對(duì)象上的方法,dot和cross方法分別對(duì)應(yīng)的就是向量的點(diǎn)乘和叉乘。
接著就來編寫功能代碼。
首先是獲取canvas2d的上下文,并完成坐標(biāo)的轉(zhuǎn)換
let canvas = document.querySelector('canvas'),
ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1);因?yàn)楫嫴荚嫉淖鴺?biāo)系是以左上角為原點(diǎn),X軸向左,Y軸向下,這不符合我們?cè)跀?shù)學(xué)中常用的配置。
這里我們先通過translate方法把坐標(biāo)挪到畫布中心,再通過scale方法將坐標(biāo)系繞X軸翻轉(zhuǎn);通過這樣的轉(zhuǎn)換,就可以按照我們?cè)跀?shù)學(xué)中常見的坐標(biāo)系來操作了。
然后我們來初始化三個(gè)點(diǎn),也就是之前說的點(diǎn)A、點(diǎn)B和點(diǎn)C。
坐標(biāo)可以隨便寫,只要范圍在-256到256之間就可以。
我這里就簡(jiǎn)單定義三個(gè)在X軸上的點(diǎn),并維護(hù)在一個(gè)Map中,方便后續(xù)在canvas上顯示三個(gè)點(diǎn)的標(biāo)識(shí);后面會(huì)加一個(gè)事件監(jiān)聽來更新點(diǎn)C的坐標(biāo)。
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);然后就可以開始繪制
這里我們定義一個(gè)draw函數(shù),然后調(diào)用它。
draw();
function draw() {}好啦,到這里為止一個(gè)簡(jiǎn)單的距離展示就完成了;我們可以通過移動(dòng)鼠標(biāo)來查看最后的效果。
首先,為了看上去更清晰,我們可以把坐標(biāo)系繪制出來。
因?yàn)榻酉氯ダL制的直線比較多,這里我簡(jiǎn)單封裝一個(gè)繪制直線的方法。
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();
}然后我們來繪制坐標(biāo)系。
drawAxis();
function drawAxis() {
drawLine([-canvas.width / 2, 0], [canvas.width / 2, 0], "#333");
drawLine([0, canvas.height / 2], [0, -canvas.height / 2], "#333");
}接著我們把點(diǎn)繪制到畫布上
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();
}這里我們想把點(diǎn)的標(biāo)識(shí)通過fillText也繪制到畫布上,但由于之前坐標(biāo)被繞X軸翻轉(zhuǎn)過一次,所以直接繪制表示會(huì)導(dǎo)致文本是倒過來的,所以我們這里臨時(shí)把坐標(biāo)系翻轉(zhuǎn)回來,完成文本繪制后,再通過restore恢復(fù)回去。
現(xiàn)在我們把線段AB也繪制出來
drawBaseline();
function drawBaseline() {
drawLine(map.get('A'), map.get('B'), "blue");
}最后就是最關(guān)鍵的一步,把點(diǎn)C到線段AB和直線的距離求出來并展示在canvas畫布上
d為點(diǎn)C到線段AB的距離,dLine為點(diǎn)C到直線的距離;
result存儲(chǔ)的是AC和AB的點(diǎn)乘結(jié)果;crossProduct存儲(chǔ)的是AC和AB的叉乘結(jié)果。
根據(jù)叉乘結(jié)果,我們就可以計(jì)算出dLine的值,也就是點(diǎn)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 = `點(diǎn)C到線段AB的距離:${Math.floor(d)}, 點(diǎn)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;
}然后我們來計(jì)算點(diǎn)D的坐標(biāo):
已知:AD是AC在AB上的投影。
所以AD可以表示為這樣:(AB / |AB|) * (AC·AB / |AB|)
向量AB除以AB的模即代表和向量AB同一方向夾角的單位向量,單位向量可以簡(jiǎn)單理解為長(zhǎng)度為1的向量;
AC和AB的點(diǎn)積除以AB的模結(jié)果等于AC的模乘以兩個(gè)向量夾角的余弦值。
所以這兩個(gè)值相乘,就等于是向量AD。
通過調(diào)整上面的公式,我們可以得到AD = AB * (AC·AB / |AB|²) ,因?yàn)锳、B、C的坐標(biāo)都已知,也就可以得到向量AD的坐標(biāo)。
然后我們又知道向量AD的坐標(biāo)可以直接通過向量的減法得到,也就是:
所以我們就可以得到點(diǎn)D的坐標(biāo),即(AD.x + A.x, AD.y + A.y)。
接著我們根據(jù)AC和AB的點(diǎn)乘結(jié)果result,來繪制相應(yīng)的直線。
最后我們將結(jié)果通過fillText方法繪制到屏幕上。
function drawText(distance) {
ctx.beginPath();
ctx.save();
ctx.font = "16px serif";
ctx.scale(1, -1);
ctx.fillText(`${distance}`, -250, 240);
ctx.restore();
}AD.x = D.x - A.x
AD.y = D.y - A.y
當(dāng)result為負(fù)數(shù)時(shí),說明AC和AB夾角的余弦值大于90度
即∠CAB為鈍角,說明點(diǎn)C到線段AB的距離就是點(diǎn)C到點(diǎn)A的距離。
而當(dāng)result大于AC長(zhǎng)度的平方,也就是AC的模乘以余弦值大于AB的模,也就是說,AC在向量AB上的投影大于AB的長(zhǎng)度
那么此時(shí)∠CBA是鈍角,點(diǎn)C到線段AB的距離就是點(diǎn)C到點(diǎn)B的距離。
當(dāng)result為0時(shí),說明兩個(gè)向量互相垂直
此時(shí),點(diǎn)C在線段AB的上方或下方,點(diǎn)C到線段AB的距離就是點(diǎn)C到直線的距離。也就是我們前面求到的dLine的值。
最后我們加一個(gè)鼠標(biāo)移動(dòng)事件,動(dòng)態(tài)地更新點(diǎn)C的坐標(biāo),以及點(diǎn)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();
});
}以上就是JS可視化學(xué)習(xí)向量計(jì)算點(diǎn)到線段的距離并展示的詳細(xì)內(nèi)容,更多關(guān)于JS向量計(jì)算點(diǎn)到線段距離的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript游戲開發(fā)之《三國(guó)志曹操傳》零部件開發(fā)(四)用地圖塊拼成大地圖
小時(shí)候我們玩過拼圖游戲,是用自己的手去拼的。今天我們來研究研究用javascript來拼圖感興趣的朋友可以了解下,希望本文對(duì)你有所幫助2013-01-01
JavaScript中fromCharCode 和 fromCodePoint 的詳解與應(yīng)
本文將詳細(xì)介紹 JavaScript 中的 String.fromCharCode 和 String.fromCodePoint 方法,這兩個(gè)方法能夠幫助開發(fā)者高效地處理字符與編碼之間的轉(zhuǎn)換,理解它們的區(qū)別與應(yīng)用,將讓你的代碼更加靈活和高效,感興趣的朋友跟隨小編一起看看吧2024-12-12
JS判斷傳入函數(shù)的參數(shù)是否為空(函數(shù)參數(shù)是否傳遞)
這篇文章主要介紹了JS判斷傳入函數(shù)的參數(shù)是否為空(函數(shù)參數(shù)是否傳遞),需要的朋友可以參考下2023-05-05
JS前端接口防止重復(fù)請(qǐng)求的三種實(shí)現(xiàn)方案
前段時(shí)間心血來潮,想把項(xiàng)目的前端都做一下接口防止重復(fù)請(qǐng)求的處理,雖然大部分的接口處理我們都是加了loading的,但又不能確保真的是每個(gè)接口都加了的,下面就來總結(jié)一下這次的防重復(fù)請(qǐng)求的實(shí)現(xiàn)方案,需要的朋友可以參考下2024-03-03
微信小程序自定義彈窗滾動(dòng)與頁(yè)面滾動(dòng)沖突的解決方法
這篇文章主要為大家詳細(xì)介紹了微信小程序自定義彈窗滾動(dòng)與頁(yè)面滾動(dòng)沖突的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07

