一,坐标系
1.坐标系和坐标映射
浏览器四个图形系统的通用坐标系是:
- 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
实现坐标转换
2.Canvas实现坐标系转换
左上角的原点通过自己的原点transform和scaleAPI实现画布中间的转换,x轴为向右为正,y轴先上为正。
这样做的目的是为了更方便、更直观地计算几个图形元素的坐标,而不是在绘制之前进行大量的计算。
二,向量
视觉呈现依赖于计算机图形学,向量运算是整个计算机图形学的数学基础。
在向量运算中,向量点乘和叉乘运算除了加法表示移动点和绘制线段外,还具有特殊意义
1.对图形学习中向量的理解
我们通常用向量来表示一个点或一个线段
二维向量实际上是一个包含两个值的数组,一个是 x
坐标值,一个是 y
坐标值。向量的维度等于列表的长度
一个向量包含长度和方向信息。它的长度可以是向量的 x、y
如果用平方和平方根来表示 JavaScript
计算是:
v.length = function(){
return Math.hypot(this.x, this.y)///hypot() 回到欧几里德范数 sqrt(x*x y*y)。 };
可以使用它的方向 x
轴的夹角表示:
v.dir = function() {
return Math.atan2(this.y, this.x);///向量夹角 } //在上面的代码中,Math.atan2 值的范围是 -π到π,负数表示在 x 轴下,正数表示在 x 轴上方。
最后,根据长度和方向的定义,我们还可以推导出一组关系:
v.x = v.length * Math.cos(v.dir); x等于向量的长度*cos(向量的夹角)
v.y = v.length * Math.sin(v.dir); y等于向量的长度*sin(向量的夹角)
//知道方向和长度就能求出向量的值(x,y)
这个推论意味着一个重要的事实:我们可以很简单地构造出一个绘图向量。也就是说,如果我们希望以点 (x0, y0)
为起点,沿着某个方向画一段长度为 length
的线段,我们只需要构造出如下的一个向量就可以了。
这里的α
是与 x
轴的夹角,v
是一个单位向量(基向量),它的长度为 1
。然后我们把向量 (x0, y0) 与这个向量 v1相加,得到的就是这条线段的终点。
2.向量加法
可以看成是在空间中的平移平行
也可以看出数轴上加法的一种扩展(运算上)
3.向量数乘
-
可以看成,向量长度的数乘(倍数),这种拉伸和压缩,有时还会改变向量方式(正负)的现象,也叫向量的缩放scaling
-
倍数,也称为标量scalars
-
数字在向量中的主要作用就是缩放变量
4.向量点乘
[1]数学计算上的理解
//两个 N 维向量 a 和 b,a = [a1, a2, ...an],b = [b1, b2, ...bn]
a•b = a1*b1 + a2*b2 + ... + an*bn
两种特殊情况:
-
第一种是,当 a、b 两个向量平行时,它们的夹角就是 0°,那么
a·b=|a|*|b|
,用 JavaScript 代码表示就是:a.x * b.x + a.y * b.y === a.length * b.length;
-
第二种是,当 a、b 两个向量垂直时,它们的夹角就是 90°,那么
a·b=0
,用 JavaScript 代码表示就是:a.x * b.x + a.y * b.y === 0;
[2]几何理解
投影为垂直投影
- 当向量
w
和向量v
的方向相反时,它们的点积的值为负的- 当向量w和向量v的方向大致相同时,它们的点积为正的
- 当向量w和向量v相互垂直时,意味着一个向量在另一个向量的投影为零向量,它们的点积为零
点积与顺序无关,也就是,谁投影到谁上都是相同的
首先假设 向量
v
和 向量w
长度相同,利用对称轴,两个向量互相的投影相等;接下来如果你,对称性被破坏,但是,最终也没变,一动图胜千言
我们有一个从二维空间到数轴的线性变换,它并不是由向量数值或点运算定义得到的。而是将通过空间投影到给定数轴上来定义得到的,但是因为这个变换是线性的,所以它必然 可以使用某个1x2的矩阵来描述,又因为1x2矩阵与二维向量相乘的计算过程和转置矩阵并求点积的计算过程相同,所以这个投影变换必然会与某个二维向量相关。
- 方便
- 更进一步,两个,就是将
- 向量仿佛是一个。对一般人类来说,想象空间中的向量比想象这个空间移动到数轴上更加容易
5.向量的叉乘
叉乘和点乘有两点不同:
叉乘在数学上的计算:
假设,现在有两个三维向量 a(x1, y1, z1)
和 b(x2, y2, z2)
,那么,a
与 b
的叉积可以表示为一个如下图的行列式:
//其中 i、j、k 分别是 x、y、z 轴的单位向量。我们把这个行列式展开,就能得到如下公式
a X b = [y1 * z2 - y2 * z1, - (x1 * z2 - x2 * z1), x1 * y2 - x2 * y1]
我们计算这个公式,得到的值还是一个三维向量,它的方向垂直于 a、b 所在平面。因此,我们刚才说的二维空间中,向量 a、b 的叉积方向就是垂直纸面朝向我们的。
确定叉积的方向:
右手系中向量叉乘的方向就是右手拇指的方向,那左手系中向量叉乘的方向自然就是左手拇指的方向了。
在了解了向量叉积的几何意义之后,我们通过向量叉积得到平行四边形面积,再除以底边长,就能得到点到向量所在直线的距离了
6.使用向量绘制曲线
先确定起始点和起始向量,然后通过旋转和向量加法来控制形状,就可以将曲线一段一段地绘制出来。但是它的缺点也很明显,就是数学上不太直观,需要复杂的换算才能精确确定图形的位置和大小。
Canvas2D 和 SVG 中都提供了画圆、椭圆、贝塞尔曲线的API,但是像WEBGL这种偏底层的则没有这些api了,需要自己封装。
原理:曲线是可以用折线来模拟的,当折线足够多时,折线图形便成为了曲线图形。
//绘制正边变型
function regularShape(edges = 3, x, y, step) {
const ret = [];
const delta = Math.PI * (1 - (edges - 2) / edges);
let p = new Vector2D(x, y);
const dir = new Vector2D(step, 0);
ret.push(p);
for(let i = 0; i < edges; i++) {
p = p.copy().add(dir.rotate(delta));
ret.push(p);
}
return ret;
}
draw(regularShape(3, 128, 128, 100)); // 绘制三角形
draw(regularShape(6, -64, 128, 50)); // 绘制六边形
draw(regularShape(11, -64, -64, 30)); // 绘制十一边形
draw(regularShape(60, 128, -64, 6)); // 绘制六十边形
三,参数方程
:通过封装函数来实现,使用参数方程能够避免向量绘制的缺点,因此是更常用的绘制方式。使用参数方程绘制曲线时,我们既可以使用有规律的曲线参数方程来绘制这些规则曲线,还可以使用二阶、三阶贝塞尔曲线来在起点和终点之间构造平滑曲线。
1.圆
圆的参数方程
图形绘制代码
const TAU_SEGMENTS = 60;
const TAU = Math.PI * 2;
function arc(x0, y0, radius, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const ret = ang === TAU ? [] : [[x0, y0]];
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const x = x0 + radius * Math.cos(startAng + ang * i / segments); //方程
const y = y0 + radius * Math.sin(startAng + ang * i / segments); //方程
ret.push([x, y]);
}
return ret;
}
draw(arc(0, 0, 100));
2.椭圆
椭圆方程:
const TAU_SEGMENTS = 60;
const TAU = Math.PI * 2;
function ellipse(x0, y0, radiusX, radiusY, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const ret = ang === TAU ? [] : [[x0, y0]];
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const x = x0 + radiusX * Math.cos(startAng + ang * i / segments);
const y = y0 + radiusY * Math.sin(startAng + ang * i / segments);
ret.push([x, y]);
}
return ret;
}
draw(ellipse(0, 0, 100, 50));
3.抛物线
抛物线方程:
const LINE_SEGMENTS = 60;
function parabola(x0, y0, p, min, max) {
const ret = [];
for(let i = 0; i <= LINE_SEGMENTS; i++) {
const s = i / 60;
const t = min * (1 - s) + max * s;
const x = x0 + 2 * p * t ** 2;
const y = y0 + 2 * p * t;
ret.push([x, y]);
}
return ret;
}
draw(parabola(0, 0, 5.5, -10, 10));
4.常见曲线
如果我们为每一种曲线都分别对应实现一个函数,就会非常笨拙和繁琐。那为了方便,我们可以用函数式的编程思想,封装一个更简单的 JavaScript 参数方程绘图模块,以此来绘制出不同的曲线。这个绘图模块的使用过程主要分为三步。
第一步,我们实现一个叫做 parametric
的高阶函数,它的参数分别是 x、y 坐标和参数方程。
第二步,parametric
会返回一个函数,这个函数会接受几个参数,比如,start、end 这样表示参数方程中关键参数范围的参数,以及 seg 这样表示采样点个数的参数等等。在下面的代码中,当 seg 默认 100 时,就表示在 start、end 范围内采样 101(seg+1)个点,后续其他参数是作为常数传给参数方程的数据。
第三步,我们调用 parametric
返回的函数之后,它会返回一个对象。这个对象有两个属性:一个是 points,也就是它生成的顶点数据;另一个是 draw 方法,我们可以利用这个 draw 方法完成绘图。
// 根据点来绘制图形
function draw(points, context, {
strokeStyle = 'black',
fillStyle = null,
close = false,
} = {
}) {
context.strokeStyle = strokeStyle;
context.beginPath();
context.moveTo(...points[0]);
for(let i = 1; i < points.length; i++) {
context.lineTo(...points[i]);
}
if(close) context.closePath();
if(fillStyle) {
context.fillStyle = fillStyle;
context.fill();
}
context.stroke();
}
export function parametric(xFunc, yFunc) {
return function (start, end, seg = 100, ...args) {
const points = [];
for(let i = 0; i <= seg; i++) {
const p = i / seg;
const t = start * (1 - p) + end * p;
const x = xFunc(t, ...args); // 计算参数方程组的x
const y = yFunc(t, ...args); // 计算参数方程组的y
points.push([x, y]);
}
return {
draw: draw.bind(null, points),
points,
};
};
}
利用绘图模块,我们就可以绘制出各种有趣的曲线了。比如,我们可以很方便地绘制出抛物线,代码如下:
// 抛物线参数方程
const para = parametric(
t => 25 * t,
t => 25 * t ** 2,
);
// 绘制抛物线
para(-5.5, 5.5).draw(ctx);
再比如,我们可以绘制出阿基米德螺旋线,代码如下:
const helical = parametric(
(t, l) => l * t * Math.cos(t),
(t, l) => l * t * Math.sin(t),
);
helical(0, 50, 500, 5).draw(ctx, {
strokeStyle: 'blue'});
绘制星形线
const star = parametric(
(t, l) => l * Math.cos(t) ** 3,
(t, l) => l * Math.sin(t) ** 3,
);
star(0, Math.PI * 2, 50, 150).draw(ctx, {
strokeStyle: 'red'});
[1]画贝塞尔曲线
前面我们说的这些曲线都比较常见,它们都是符合某种固定数学规律的曲线。但生活中还有很多不规则的图形,无法用上面这些规律的曲线去描述。那我们该如何去描述这些不规则图形呢?贝塞尔曲线(Bezier Curves)就是最常见的一种解决方式。
我们可以用 parametric 构建并绘制二阶贝塞尔曲线,代码如下所示:
const quadricBezier = parametric(
(t, [{
x: x0}, {
x: x1}, {
x: x2}]) => (1 - t) ** 2 * x0 + 2 * t * (1 - t) * x1 + t ** 2 * x2,
(t, [{
y: y0}, {
y: y1}, {
y: y2}]) => (1 - t) ** 2 * y0 + 2 * t * (1 - t) * y1 + t ** 2 * y2,
);
const p0 = new Vector2D(0, 0);
const p1 = new Vector2D(100, 0);
p1.rotate(0.75);
const p2 = new Vector2D(200, 0);
const count = 30;
for(let i = 0; i < count; i++) {
// 绘制30条从圆心出发,旋转不同角度的二阶贝塞尔曲线
p1.rotate(2 / count * Math.PI);
p2.rotate(2 / count * Math.PI);
quadricBezier(0, 1, 100, [
p0,
p1,
p2,
]).draw(ctx);
}
在上面的代码中,我们绘制了 30 个二阶贝塞尔曲线,它们的起点都是 (0,0),终点均匀分布在半径 200 的圆上,控制点均匀地分布在半径 100 的圆上。最终,实现的效果如下图所示
[2]贝塞尔曲线绘制Catmull–Rom
总的来说,贝塞尔曲线对于可视化,甚至整个计算机图形学都有着极其重要的意义。因为它能够针对一组确定的点,在其中构造平滑的曲线,这也让图形的实现有了更多的可能性。而且,贝塞尔曲线还可以用来构建 Catmull–Rom 曲线。Catmull–Rom 曲线也是一种常用的曲线,它可以平滑折线,我们在数据统计图表中经常会用到它。
四,多边形
在图形系统中,我们最终看到的丰富多彩的图像,都是由多边形构成的。换句话说,不论是 2D 图形还是 3D 图形,经过投影变换后,在屏幕上输出的都是多边形。
1.图形学中多边形的定义
多边形可以定义为由三条或三条以上的线段首尾连接构成的平面图形,其中,每条线段的端点就是多边形的顶点,线段就是多边形的边。
多边形又可以分为形和
如果一个多边形的每条边除了相邻的边以外,不和其他边相交,那它就是简单多边形,否则就是复杂多边形。
而简单多边形又分为凸多边形和凹多边形,我们主要是看简单多边形的内角来区分的。如果一个多边形中的每个内角都不超过 180°,那它就是凸多边形,否则就是凹多边形。
2.多边形的填充和边界判定
在图形系统中绘制多边形的时候,最常用的功能是填充多边形,也就是用一种颜色将多边形的内部填满。
除此之外,在可视化中用户经常要用鼠标与多边形进行交互,这就要涉及多边形的边界判定。
3.不同的图形系统如何填充多边形
不同的图形系统会用不同的方法来填充多边形。比如说,在 SVG 和 Canvas2D 中,就都内置了填充多边形的 API。在 SVG 中,我们可以直接给元素设置 fill 属性来填充,那在 Canvas2D 中,我们可以在绘图指令结束时调用 fill() 方法进行填充。
而在 WebGL
中,我们是用三角形图元来快速填充的。
[1]Canvas2D 填充多边形
Canvas2D 填充多边形可以总结为五步
第一步,构建多边形的顶点。这里我们直接构造 5 个顶点
[2]WebGL 填充多边形
在 WebGL 中,虽然没有提供自动填充多边形的方法,但是我们可以用三角形这种基本图元来快速地填充多边形。因此,在 WebGL 中填充多边形的第一步,就是将多边形分割成多个三角形。
这种将多边形分割成若干个三角形的操作,在图形学中叫做
对简单多边形尤其是凸多边形的三角剖分比较简单,而复杂多边形由于有边的相交和面积重叠区域,所以相对困难许多,因为那些算法会比较复杂涉及很多图形学的底层数学知识
这里,我们就直接利用 GitHub
上的一些成熟的库(常用的如Earcut
、Tess2.js
以及cdt2d
),来对多边形进行三角剖分就可以了。
例如利用Earcut
库来进行三角剖分
//上图的顶点数据 const vertices = [ [-0.7, 0.5 标签:
2d型位移变送器