跳转至

05 如何用向量和坐标系描述点和线段?

你好,我是月影。

为什么你做了很多可视化项目,解决了一个、两个、三个甚至多个不同类型的图表展现之后,还是不能系统地提升自己的能力,在下次面对新的项目时依然会有各种难以克服的困难?这是因为你陷入了细节里。

什么是细节?简单来说,细节就是各种纯粹的图形学问题。在可视化项目里,我们需要描述很多的图形,而描述图形的顶点、边、线、面、体和其他各种信息有很多不同的方法。并且,如果我们使用不同的绘图系统,每个绘图系统又可能有独特的方式或者特定的API,去解决某个或某类具体的问题。

正因为有了太多可以选择的工具,我们也就很难找到最恰当的那一个。而且如果我们手中只有解决具体问题的工具,没有统一的方法论,那我们也无法一劳永逸地解决问题的根本

因此,我们要建立一套与各个图形系统无关联的、简单的基于向量和矩阵运算的数学体系,用它来描述所有的几何图形信息。这就是我在数学篇想要和你讨论的主要问题,也就是如何建立一套描述几何图形信息的数学体系,以及如何用这个体系来解决我们的可视化图形呈现的问题

那这一节课,我们先学习用坐标系与向量来描述基本图形的方法,从如何定义和变换图形的直角坐标系,以及如何运用向量表示点和线段这两方面讲起。

坐标系与坐标映射

首先,我们来看看浏览器的四个图形系统通用的坐标系分别是什么样的。

HTML采用的是窗口坐标系,以参考对象(参考对象通常是最接近图形元素的position非static的元素)的元素盒子左上角为坐标原点,x轴向右,y轴向下,坐标值对应像素值。

SVG采用的是视区盒子(viewBox)坐标系。这个坐标系在默认情况下,是以svg根元素左上角为坐标原点,x轴向右,y轴向下,svg根元素右下角坐标为它的像素宽高值。如果我们设置了viewBox属性,那么svg根元素左上角为viewBox的前两个值,右下角为viewBox的后两个值。

Canvas采用的坐标系我们比较熟悉了,它默认以画布左上角为坐标原点,右下角坐标值为Canvas的画布宽高值。

WebGL的坐标系比较特殊,是一个三维坐标系。它默认以画布正中间为坐标原点,x轴朝右,y轴朝上,z轴朝外,x轴、y轴在画布中范围是-1到1。

尽管这四个坐标系在原点位置、坐标轴方向、坐标范围上有所区别,但都是直角坐标系,所以它们都满足直角坐标系的特性:不管原点和轴的方向怎么变,用同样的方法绘制几何图形,它们的形状和相对位置都不变。

为了方便处理图形,我们经常需要对坐标系进行转换。转换坐标系可以说是一个非常基础且重要的操作了。正因为这四个坐标系都是直角坐标系,所以它们可以很方便地相互转化。其中,HTML、SVG和Canvas都提供了transform的API能够帮助我们很方便地转换坐标系。而WebGL本身不提供tranform的API,但我们可以在shader里做矩阵运算来实现坐标转换,WebGL的问题我们在后续课程会有专门讨论,今天我们先来说说其他三种。那接下来我们就以Canvas为例,来看看用transform API怎样进行坐标转换。

如何用Canvas实现坐标系转换?

假设,我们要在宽512 * 高256的一个Canvas画布上实现如下的视觉效果。其中,山的高度是100,底边200,两座山的中心位置到中线的距离都是80,太阳的圆心高度是150。

当然,在不转换坐标系的情况下,我们也可以把图形绘制出来,但是要经过顶点换算,下面我们就来说一说这个过程。

首先,因为Canvas坐标系默认的原点是左上角,底边的y坐标是256,而山的高度是100,所以山顶点的y坐标是256 - 100 = 156。而因为太阳的高度是150,所以太阳圆心的y坐标是256 - 150 = 106。

然后,因为x轴中点的坐标是512 / 2 = 256,所以两座山顶点的x坐标分别是256 - 80和256 + 80,也就是176和336。又因为山是等腰三角形,它的底边是200,所以两座山底边的x坐标计算出来,分别是 76、276、236、436(176 - 100 =76、176 + 100=276、336 - 100=236、 336 + 100=436)。

计算出这些坐标之后,我们很容易就可以将这个图画出来了。不过,为了增加一些趣味性,我们用一个Rough.js的库,绘制一个手绘风格的图像(Rough.js库的API和Canvas差不多,绘制出来的图形比较有趣)。绘制的代码如下所示:

const rc = rough.canvas(document.querySelector('canvas'));
const hillOpts = {roughness: 2.8, strokeWidth: 2, fill: 'blue'};
rc.path('M76 256L176 156L276 256', hillOpts);
rc.path('M236 256L336 156L436 256', hillOpts);
rc.circle(256, 106, 105, {
  stroke: 'red',
  strokeWidth: 4,
  fill: 'rgba(255, 255, 0, 0.4)',
  fillStyle: 'solid',
});

最终,我们绘制出的图形效果如下所示:

到这里,我们通过简单的计算就绘制出了这一组图形。但你也能够想到,如果每次绘制都要花费时间在坐标换算上,这会非常不方便。所以,为了解决这个问题,我们可以采用坐标系变换来代替坐标换算。

这里,我们给Canvas的2D上下文设置一下transform变换。我们经常会用到两个变换:translate和scale。

首先,我们通过translate变换将Canvas画布的坐标原点,从左上角(0, 0)点移动至(256, 256)位置,即画布的底边上的中点位置。接着,以移动了原点后新的坐标为参照,通过scale(1, -1)将y轴向下的部分,即y>0的部分沿x轴翻转180度,这样坐标系就变成以画布底边中点为原点,x轴向右,y轴向上的坐标系了。

执行了这个坐标变换,也就是让坐标系原点在中间之后,我们就可以更方便、直观地计算出几个图形元素的坐标了。

两个山顶的坐标就是 (-80, 100) 和 (80, 100),山脚的坐标就是 (-180, 0)、(20, 0)、(-20, 0)、(180, 0),太阳的中心点的坐标就是(0, 150)。那么更改后的代码如下所示。

const rc = rough.canvas(document.querySelector('canvas'));
const ctx = rc.ctx;
ctx.translate(256, 256);
ctx.scale(1, -1);

const hillOpts = {roughness: 2.8, strokeWidth: 2, fill: 'blue'};

rc.path('M-180 0L-80 100L20 0', hillOpts);
rc.path('M-20 0L80 100L180 0', hillOpts);

rc.circle(0, 150, 105, {
  stroke: 'red',
  strokeWidth: 4,
  fill: 'rgba(255,255, 0, 0.4)',
  fillStyle: 'solid',
});

好了,现在我们就完成了坐标变换。但是因为这个例子要绘制的图形很少,所以还不太能体现使用坐标系变换的好处。不过,你可以想一下,在可视化的许多应用场景中,我们都要处理成百上千的图形。如果这个时候,我们在原始坐标下通过计算顶点来绘制图形,计算量会非常大,很麻烦。那采用坐标变换的方式就是一个很好的优化思路,它能够简化计算量,这不仅让代码更容易理解,也可以节省CPU运算的时间

理解直角坐标系的坐标变换之后,我们再来说说直角坐标系里绘制图形的方法。那不管我们用什么绘图系统绘制图形,一般的几何图形都是由点、线段和面构成。其中,点和线段是基础的图元信息,因此,如何描述它们是绘图的关键

如何用向量来描述点和线段?

那在直角坐标系下,我们是怎么表示点和线段的呢?我们一般是用向量来表示一个点或者一个线段。

前面的例子因为包含x、y两个坐标轴,所以它们构成了一个绘图的平面。因此,我们可以用二维向量来表示这个平面上的点和线段。二维向量其实就是一个包含了两个数值的数组,一个是x坐标值,一个是y坐标值。

假设,现在这个平面直角坐标系上有一个向量v。向量v有两个含义:一是可以表示该坐标系下位于(x, y)处的一个点;二是可以表示从原点(0,0)到坐标(x,y)的一根线段。

接下来,为了方便你理解,我们先来回顾一下关于向量的数学知识。

首先,向量和标量一样可以进行数学运算。举个例子,现在有两个向量,v1和v2,如果让它们相加,其结果相当于将v1向量的终点(x1, y1),沿着v2向量的方向移动一段距离,这段距离等于v2向量的长度。这样,我们就可以在平面上得到一个新的点(x1 + x2, y1 + y2),一条新的线段[(0, 0), (x1 + x2, y1 + y2)],以及一段折线:[(0, 0), (x1, y1) , (x1 + x2, y1 + y2)]。

其次,一个向量包含有长度和方向信息。它的长度可以用向量的x、y的平方和的平方根来表示,如果用JavaScript来计算,就是:

v.length = function(){return Math.hypot(this.x, this.y)};

它的方向可以用与x轴的夹角来表示,即:

v.dir = function() { return Math.atan2(this.y, this.x);}

在上面的代码里,Math.atan2的取值范围是-π到π,负数表示在x轴下方,正数表示在x轴上方。

最后,根据长度和方向的定义,我们还能推导出一组关系式:

v.x = v.length * Math.cos(v.dir);
v.y = v.length * Math.sin(v.dir);

这个推论意味着一个重要的事实:我们可以很简单地构造出一个绘图向量。也就是说,如果我们希望以点(x0, y0)为起点,沿着某个方向画一段长度为length的线段,我们只需要构造出如下的一个向量就可以了。

这里的α是与x轴的夹角,v是一个单位向量,它的长度为1。然后我们把向量(x0, y0)与这个向量v1相加,得到的就是这条线段的终点。这么讲还是比较抽象,我们看一个例子。

实战演练:用向量绘制一棵树

我们用前面学到的向量知识来绘制一棵随机生成的树,想要生成的效果如下:

我们还是用Canvas2D来绘制。首先是坐标变换,原理前面讲过,我就不细说了。这里,我们要做的变换是将坐标原点从左上角移动到左下角,并且让y轴翻转为向上。

ctx.translate(0, canvas.height);
ctx.scale(1, -1);
ctx.lineCap = 'round';

然后,我们定义一个画树枝的函数 drawBranch。

function drawBranch(context, v0, length, thickness, dir, bias) {
  ...
}

这个函数有六个参数:

  • context是我们的Canvas2D上下文
  • v0是起始向量
  • length是当前树枝的长度
  • thickness是当前树枝的粗细
  • dir是当前树枝的方向,用与x轴的夹角表示,单位是弧度。
  • bias是一个随机偏向因子,用来让树枝的朝向有一定的随机性

因为v0是树枝的起点坐标,那根据前面向量计算的原理,我们创建一个单位向量(1, 0),它是一个朝向x轴,长度为1的向量。然后我们旋转dir弧度,再乘以树枝长度length。这样,我们就能计算出树枝的终点坐标了。代码如下:

 const v = new Vector2D(1, 0).rotate(dir).scale(length);
 const v1 = v0.copy().add(v);

向量的旋转是向量的一种常见操作,对于二维空间来说,向量的旋转可以定义成如下方法(这里我们省略了数学推导过程,有兴趣的同学可以去看一下数学原理)。这个方法我们后面还会经常用到,你先记一下,后续我们讲到仿射变换的时候,会有更详细的解释。

class Vector2D {
  ...  
  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;
  }
}

我们可以从一个起始角度开始递归地旋转树枝,每次将树枝分叉成左右两个分枝:

  if(thickness > 2) {
    const left = dir + 0.2;
    drawBranch(context, v1, length * 0.9, thickness * 0.8, left, bias * 0.9);
    const right = dir - 0.2;
    drawBranch(context, v1, length * 0.9, thickness * 0.8, right, bias * 0.9);
  }

这样,我们得到的就是一棵形状规律的树。

接着我们修改代码,加入随机因子,让迭代生成的新树枝有一个随机的偏转角度。

  if(thickness > 2) {
    const left = Math.PI / 4 + 0.5 * (dir + 0.2) + bias * (Math.random() - 0.5);
    drawBranch(context, v1, length * 0.9, thickness * 0.8, left, bias * 0.9);
    const right = Math.PI / 4 + 0.5 * (dir - 0.2) + bias * (Math.random() - 0.5);
    drawBranch(context, v1, length * 0.9, thickness * 0.8, right, bias * 0.9);
  }

这样,我们就可以得到一棵随机的树。

最后,为了美观,我们再随机绘制一些花瓣上去,你也可以尝试绘制其他的图案到这棵树上。

  if(thickness < 5 && Math.random() < 0.3) {
    context.save();
    context.strokeStyle = '#c72c35';
    const th = Math.random() * 6 + 3;
    context.lineWidth = th;
    context.beginPath();
    context.moveTo(...v1);
    context.lineTo(v1.x, v1.y - 2);
    context.stroke();
    context.restore();
  }

这样,我们就实现了绘制一棵随机树的方法。

它的完整代码在GitHub仓库,你可以研究一下。这里面最关键的一步就是前面的向量操作,为了实现向量的rotate、scale、add等方法,我封装了一个简单的库Vector2d.js,你也可以在代码仓库中找到它。

向量运算的意义

实际上,在我们的可视化项目里,直接使用向量的加法、旋转和乘法来构造线段绘制图形的情形并不多。这是因为,在一般情况下,数据在传给前端的时候就已经计算好了,我们只需要拿到数据点的信息,根据坐标变换进行映射,然后直接用映射后的点来绘制图形即可。

既然这样,为什么我们在这里又要强调向量操作的重要性呢?虽然我们很少直接使用向量构造线段来完成绘图,但是向量运算的意义并不仅仅只是用来算点的位置和构造线段,这只是最初级的用法。我们要记住,可视化呈现依赖于计算机图形学,而向量运算是整个计算机图形学的数学基础。

而且,在向量运算中,除了加法表示移动点和绘制线段外,向量的点乘、叉乘运算也有特殊的意义。课后我会给你出一道有挑战性的思考题 ,让你能更深入地理解向量运算的现实意义,在下一节课里我会给你答案。

要点总结

这一节课, 我们以Canvas为例学习了坐标变换,以及用向量描述点和线段的原理和方法。

一般来说,采用平面直角坐标系绘图的时候,对坐标进行平移等线性变换,并不会改变坐标系中图形的基本形状和相对位置,因此我们可以利用坐标变换让我们的绘图变得更加容易。Canvas坐标变换经常会用到translate和scale这两个变换,它们的操作和原理都很简单,我们根据实际需求来设置就好了。

在平面直角坐标系中,我们可以定义向量来绘图。向量可以表示绘图空间中的一个点,或者连接原点的一条线段。两个向量相加,结果相当于将被加向量的终点沿着加数向量的方向移动一段距离,移动的距离等于加数向量的长度。利用向量的这个特性,我们就能以某个点为起点,朝任意方向绘制线段,从而绘制各种较复杂的几何图形了。

小试牛刀

  1. 我们已经知道如何用向量来定义一个线段,你知道如何判断两个线段的位置关系吗?假设有两个线段l1和l2,已知它们的起点和终点分别是[(x10, y10),(x11, y11)]、[(x20, y20),(x21, y21)],你能判断它们的关系吗(小提示:两个线段之间的关系有平行、垂直或既不平行又不垂直)?
  2. 已知线段[(x0, y0)、(x1, y1)],以及一个点(x2, y2),怎么求点到线段的距离?
  3. 一个平面上放置了一个扫描器,方向延y轴方向(该坐标系y轴向上),扫描器的视角是60度。假设它可以扫描到无限远的地方,那对于平面上给定的任意一个点(x,y),我们该如何判断这个点是否处于扫描范围内呢?

欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见!


源码

[1]绘制随机树的源代码
[2]坐标变换的源代码

推荐阅读

[1] 二维旋转矩阵与向量旋转推荐文档
[2] 一个有趣的绘图库:Rough.js
[3] Vector2d.js模块文档

精选留言(15)
  • gltjk 👍(36) 💬(2)

    用向量的点乘、叉乘概念重新梳理小试牛刀的三题: 线段 A1B1 与线段 A2B2 的关系 { 如果 |A1B1| 或 |A2B2| 为 0 ,说明线段退化成点,无法判断关系 如果 A1B1·A2B2 为 0,说明夹角的余弦值为 0,二者垂直 如果 |A1B1×A2B2| 为 0,说明夹角的正弦值为 0,二者方向一致,可能平行也可能重合 { 如果 |A1B1×A1A2| 不为0,说明四点不共线,排除掉重合的情况,二者平行 } 其他情况即为既不平行也不垂直 } 点 P 到线段 AB 的距离 { 如果 |AB| 为 0,说明线段退化成点,|AP| 就是距离 如果 AP·AB: <0,说明 P 到 AB 的投影在线段 BA 的延长线上,∠PAB 为钝角,|AP| 就是距离 >|AB|,说明 P 到 AB 的投影在线段 AB 的延长线上,∠PBA 为钝角,|BP| 就是距离 其他情况,说明 P 到 AB 的投影在线段 AB 上,用 |AP×AB| 除以 |AB| 即可算出距离(外积模的几何意义 |absinθ| 就是平行四边形的面积,除以底得到高) } 点 P 与扫描角度 α 的关系 { 如果 α>2π,360°全覆盖,肯定能扫描到 如果 α<0,当前仅当点 P 与原点重合(即 P 的模为 0)时才能扫描到 把一四象限里的边上从原点出发的向量定为 A,二三象限里的定为 B(如果 α=0,虽然二者不在象限里,不过此时方向一致,无所谓了) 如果 |AxP|=0 或 |PxB|=0,说明 P 与 A 或 B 方向一致,刚好在边缘上,能扫描到 如果 α>π,可以假设坐标系上下翻转,有一个视角为 2π-α 的扫描器在扫描 P 关于 x 轴的映射 P',如果 P' 能被扫描到,说明 P 扫描不到,反之亦然 其余情况,如果要扫描到,必须要让从 A 到 B 逆时针扫描时经过 P,即 AxP 和 PxB 的方向都要与 z 轴正方向一致(用右手螺旋定则可以判断) } 代码如下(在之前的代码上修改的): https://codepen.io/gltjk/pen/eYJyOeR

    2020-07-02

  • gltjk 👍(5) 💬(1)

    今天的小试牛刀完全是中学数学啊…… 1. 用Δy/Δx计算斜率,平行是斜率相等,垂直是斜率乘积为-1。所以: 平行:Δy1/Δx1=Δy2/Δx2 垂直:(Δy1/Δx1)(Δy2/Δx2)=-1 为了避免出现分母为零(即平行于y轴,斜率无穷大)的情况,可以把除法换成乘法: 平行:Δx1Δy2-Δx2Δy1=0 垂直:Δx1Δx2+Δy1Δy2=0 不过要考虑一个边缘情况,如果两个线段有一个长度为零了(退化成点),就没法判断了。 2. 设线段为AB,其中A(x0,y0),B(x1,y1),点P(x2,y2) 正常思路是直接套用点到直线距离公式 d=|Ax0+By0+C|/Math.hypot(A,B),其中A、B、C为直线方程Ax+By+C=0里的系数,(x0,y0)为点的坐标。 用两点式写出直线方程,(y-y0)/(x-x0)=(y1-y0)/(x1-x0) 换成Ax+By+C=0的形式并排除分母为0的情况:(y1-y0)x+(x0-x1)y-(y1-y0)x0-(x0-x1)y0=0 观察系数得A=y1-y0,B=x0-x1,c=-(y1-y0)x0-(x0-x1)y0=-Ax0-By0 把A、B、C及(x2,y2)代入即可。 不过这里有个坑,给的不是直线,而是线段,线段是不能无限延长的,所以过点到直线作垂线与直线的交点不一定在线段上。 如果遇到这种情况,即角A或角B是钝角,就只能取PA和PB的较小值。 判断是否为钝角,可以用邻边平方和减去对边平方,结果为负数说明是钝角(正数是锐角,零是直角——刚好就是勾股定理了)。 另外,还有一个边缘情况,就是线段的两个点重合,与第一题不同,这时候的距离还是可以算的(此时PA=PB)。 3. 模仿文中思路用反正切函数Math.atan2()取到范围是[-PI, PI]的角度值,然后与扫描范围比较即可。 这里我把扫描范围扩展为任意实数的角度,并允许扫描到原点(如果角度为负则只能扫描到原点)。 然后对扫描范围的一半θ进行分类讨论: 超过π,说明360°全方位覆盖,肯定扫描得到; 没到π/2,扫描范围在[-π,0]之中,直接和反正切值比较即可; 其余情况,扫描范围起点在第四象限,终点在第三象限,被扫描的点如果在三四象限要大于起点或小于终点,如果在一二象限肯定能扫描到(也肯定大于起点)。 综上,因为数字比较多,这次代码用TS完成,同时顺便体验了一下Vue 3的Composition API: https://codepen.io/gltjk/pen/OJMOOJR

    2020-07-01

  • 👍(4) 💬(1)

    仿佛回到了中学的解析几何题,感谢老师讲解向量的两个含义,但我对向量有些自己的理解,正好有肋于解决老师今天的思考题,下面说说我的思路: 1.1:若对此题做一个等效替代的话,其实不必拘泥于线段,可以延伸到线段所在的直线,这两条直线是否平行或垂直与原来的两条线段的关系是一致的 1.2:直线的关系可以理解为方向上的关系,既然向量可以表示方向,那么此题就可以进一步用向量来替代原来的线段,例如用v1替代线段l1,值为(x11-x10,y11-y10)。 *1.2.1:若不理解的话,就当是把原来的两条直线平移至经过坐标原点吧,因为平移不影响直线平等或垂直的关系 1.2.2:为方便书写与查看,设一些别名吧:v1=(X1,Y1),其中X1=x11-x10,Y1=y11-y10;v2=(X2,Y2),其中X2=x21-x20,Y2=y21-y20 1.3:向量垂直的条件是点积为0,所以这里判断垂直的条件是X1*X2 + Y1*Y2 == 0 1.4:平行的条件我想说得简单点,退回到直线就是它们与x轴的夹角相等,也就是斜率相等,所以条件是X1*Y2 == X2*Y1 2.1:与第1题一样的替代,距离等于该点到线段所在直线的距离,那关键就是在直线上找一个点,这个点与指定点的连成的直线与原来的直线垂直,最后要求的距离就是这两个点的距离了 2.2:同样,用向量替代题中的线段就是:v1=(X1,Y1),其中X1=x1-x0,Y1=y1-y0,然后凑一个与此向量垂直的向量v2出来,简单做法就是v2=(-Y1,X1), 代入点积公式必得0 2.2.1:当然v2也可以是(Y1,-X1),这里不妨用前一个,效果是一样的 *2.3:本题我对向量的理解是:从某点到另一点的增量,即二维的delta。借此就可以算出与题中线段垂直的直线上的另一个点,就是把前面算出的垂直向量加给点(x2,y2),就得到了(x2-Y1,y2+X1),即(x2-y1+y0,y2+x1-x0) 2.4:两点决定一直线,基于这两个点,我们就可以算出那条垂直直线的方程了,再加上题中线段的直线方程,那么这个关键点就是这两条直线的交点,问题转化为解二元一次方程组了。 2.5:最后距离就等于这个交点到(x2,y2)的距离 3.1:此题我的说法不太严谨,先这么简化:把扫描器的两条边看成两个向量v1=(X1,Y1),假定是左边那条;v2=(X2,Y2),假定是右边那条。从坐标原点到目标点看作向量v0(X0,Y0)。 3.2:那么目标点落在扫描器范围内的条件就是:v0在v1右边,同时也在v2的左边。(对于左边右边的说法我就先不给出严谨的说明了,不知道有没有专业术语) *3.3:重合的向量可以弱化理解为平行的向量,基于第一题的结论,假如v0与v1重合,就会有X0*Y1 == X1*Y0,即X0*Y1-X1*Y0 == 0, *3.3.1:那要是X0*Y1-X1*Y0的值不等于0呢?,那情况就是v0在v1的左边或右边了。 3.3.2:X0*Y1-X1*Y0这个式子是带方向性的,姑且定义为从v0旋转到v1,值有正负(反之X1*Y0-X0*Y1就是从v1旋转到v0) 3.4:那么把3.2的说法进一步数字化就是从v0旋转到v1与从v0旋转到v2互为相反数。(其实其中一个为0也行,表于目标点落在了边界上) 3.5:最后得出的公式化条件就是:(X0*Y1-X1*Y0) * (X0*Y2-X2*Y0) <= 0

    2020-07-01

  • Cailven 👍(4) 💬(1)

    那棵树让我想到了曾经用分形几何在webgl里玩生成艺术的经历。正所谓数学不好没法搞艺术,刚巧这句话就表现在图形学和视觉可视化所谓的创意编程这个范畴里。

    2020-07-01

  • weineel 👍(2) 💬(1)

    老师好,坐标系转换后怎么转换回来?

    2020-11-12

  • 从此刻起开始 👍(1) 💬(2)

    这vector2d.js如何用?我在pycharm copy了老师上传的源码,但运行不了

    2020-09-29

  • SMW🙏🏻 👍(1) 💬(2)

    这个方法的逻辑没有看懂,大佬能否解释一下 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; } 我是这么实现的 rotate(rad) { const [x, y] = this const length = Math.hypot(x, y) const c = Math.cos(rad) const s = Math.sin(rad) this.x = length * c this.y = length * s return this }

    2020-07-14

  • Geek_b6af14 👍(1) 💬(2)

    对坐标变换和图形平移的好处还是没太大感觉~~~

    2020-07-13

  • 谭鹏 👍(0) 💬(2)

    画树的html文件 于兴起来没有内容 一个大白屏

    2021-05-15

  • 番薯 👍(0) 💬(1)

    1.构建向量: a=(x11-x10,y11-y10) b=(x21-x20,y21-y20) 点积a•b=0余弦值为0则垂直 点积(a•b)²=(|a||b|)² 余弦值为±1则平行 2. 三点确定两条向量 a=(x1-x0,y1-y0) c=(x2-x0,y2-y0),勾股定理|a|²+|b|²=|c|² 可求得垂直边的长度 3.通过点(x,y)的正切值 可推出与y轴之间的角度

    2021-02-06

  • 番薯 👍(0) 💬(1)

    向量旋转那个数学原理真的看晕了 后悔三角函数没学扎实...

    2021-02-06

  • 从此刻起开始 👍(0) 💬(1)

    老师,没看明白绘制随机树的源代码中的import {Vector2D} from '../common/lib/vector2d.js';是如何用的,尽管也下载了vector2d.js 文件放入pycharm中,但无法运行,这句import {Vector2D} from '../common/lib/vector2d.js'; 如何改?

    2020-09-21

  • 景儿 👍(0) 💬(1)

    在计算量相同的情况下,坐标系转换(transform、scale)的开销 对性能影响是否可以忽略不计呢?

    2020-08-26

  • sheeeeep 👍(0) 💬(1)

    前两问和大家思路基本一致,利用叉积 Q1:用起点和终点构造出两个向量,求向量的叉积来判断是否垂直,平行,交叉乃至方向是否一致 Q2: 用起点和终点构造向量,起点和给定的点构造向量,求两个向量的叉积、线段的长度来获取投影的长度,勾股定理求垂直线段长度 第三问大家似乎都是用的角度计算和比对,不确定是否可以用线性变换的思路来处理,把扫描的两条线作为基向量构造一个新的坐标系,只有一个朦胧的思路请老师指点

    2020-07-17

  • SMW🙏🏻 👍(0) 💬(1)

    向量a,向量b,θ是夹角 计算公式:a*b = |a||b|cosθ 疑问:向量的夹角和向量的模没有任何关系,为什么计算夹角余弦时,要用到向量的模

    2020-07-14