JS可视化学习向量计算点到线段的距离并展示
本文通过JavaScript和Canvas2D实现向量计算点到线段距离的可视化演示。文章解析了点乘与叉乘在几何中的应用,详细推导了点D坐标及距离公式,并提供了完整的代码实现与交互逻辑,帮助开发者理解向量在图形计算中的实际用法。
引言
本文内容建议配合配套视频观看,以获得更好的学习体验。
近期我专注于可视化领域的学习,并通过此案例巩固知识。向量运算是计算机图形学的基石,本例展示了如何利用向量计算点到线段的距离,可视化部分采用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方法将Y轴翻转。经过这样的转换,即可按照数学中常见的坐标系进行操作。
然后初始化三个点,即前文提到的点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|²)
// 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|²) ,因为A、B、C的坐标已知,即可得到向量AD的坐标。
同时,向量AD的坐标也可通过向量减法得到,即:
由此可得点D的坐标,即(AD.x + A.x, AD.y + A.y)。
接着,根据AC和AB的点乘结果result,绘制相应的直线。
最后,通过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
当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的值。
最后,添加一个鼠标移动事件,动态更新点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();
});
}以上就是JS可视化学习向量计算点到线段的距离并展示的详细内容,更多关于JS向量计算点到线段距离的资料请关注本站其它相关文章!
网页开发三者分工:HTML结构、CSS样式与JavaScript交互怎么协作
2026-11-05 12:51:22
JS解析URL参数时怎么处理中文乱码和特殊字符
2026-11-05 12:47:23
js-cookie 存储 JSON 对象:自动序列化与反序列化技巧
2026-11-05 12:36:26
marked.js 快速上手:安装与基础 Markdown 转 HTML 示例
2026-11-05 09:32:53
Phaser.js 与 PixiJS、Cocos Creator 的技术选型对比
2026-11-05 08:36:30
