3. webGL 可视化学习 —— 绘制几何图形
这章我们来学习绘制几何图形,主要包括正多边形和曲线的绘制;
1. 绘制正多边形
分别绘制三角形,正四边形,正六边形,正二十六边形;
绘制思路
我们采用向量的方式进行绘制,其代码如下:
function regularShape(edges = 3, x, y, step) {
const ret: Vector2D[] = [];
const delta = Math.PI - (Math.PI * (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;
}
重要公式解释:n边形内角和:Math.PI*(n-2) 每个内角:Math.PI*(n-2)/n 每个外角:Math.PI - Math.PI*(n-2)/n函数参数解释:
参数 | 解释 |
---|---|
edges | 正多边形的边数 |
x | x 的坐标 |
y | y的坐标; |
step | 正多边形的边长 |
绘图方法
绘图方法与之前类似,只是我用红色标记出来了,最开始的那条边;
function draw(points: Vector2D[], strokeStyle = "black", fillStyle = null) {
ctx.strokeStyle = strokeStyle;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
ctx.stroke();
// 首边绘制成红色;
if (points.length > 2) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.strokeStyle = "red";
ctx.stroke();
ctx.closePath();
}
}
绘制图形
// 绘制x轴
draw([new Vector2D(-256, 0), new Vector2D(256, 0)]);
// 绘制y轴
draw([new Vector2D(0, -256), new Vector2D(0, 256)]);
//绘制三边形
draw(regularShape(3, 150, 50, 100));
//绘制四边形
draw(regularShape(4, -50, 50, 100));
//绘制六边形
draw(regularShape(6, -100, -150, 50));
//绘制二十五边形
draw(regularShape(25, 128, -158, 15));
特点
根据观察,我们会发现正多边形的边数越多,它就会越来越接近一个圆,我们用极限的思想,就可以得出一个结论,主要我们绘制的边数足够多,它看起来就会像一个圆。
缺点
-
定义边数、起点、一条边的长度,这就和我们通常的使用习惯,也就是定义边数、中心和半径不符。 -
如果我们按照现在这种定义方式绘图,是很难精确对应到图形的位置和大小的。 -
regularShape 可以绘制正几何图形。但是对于椭圆、抛物线、贝塞尔曲线等其他曲线的绘制就没法实现了。
2. 绘制曲线
绘制曲线我们采用公式法,因为很多曲线是有曲线公式的
常见方程式
圆的方程:
椭圆的方程:
抛物线的方程:
代码实现:
圆的代码实现:
function arc(
x0: number,
y0: number,
radius,
startAng?: number,
endAng?: number
);
function arc(
x0: number,
y0: number,
radius,
startAng: number = 0,
endAng: number = Math.PI * 2
) {
const SEGMENTS = 30; //切分为多少块;
const deg = Math.PI * 2;
const ang = Math.min(deg, endAng - startAng); // 判断是否画整圆
const ret = ang === deg ? [] : [new Vector2D(x0, y0)];
const segments = Math.round((SEGMENTS * ang) / deg); //具体多少个切成多少片
for (let i = 0; i <= segments; i++) {
const p = i / segments;
const t = startAng + (endAng - startAng) * p;
// 核心处理逻辑
const x = x0 + radius * Math.cos(t);
const y = y0 + radius * Math.sin(t);
ret.push(new Vector2D(x, y));
}
return ret;
}
椭圆的代码实现:
// 椭圆 ellipse
// x = x0 + a*cos(ø)
// y = y0 + b*sin(ø)
function ellipse(
x0: number,
y0: number,
radiusX,
radiusY,
startAng: number = 0,
endAng: number = Math.PI * 2
) {
const SEGMENTS = 30; //切分为多少块;
const deg = Math.PI * 2;
const ang = Math.min(deg, endAng - startAng); // 判断是否画整圆
const ret = ang === deg ? [] : [new Vector2D(x0, y0)];
const segments = Math.round((SEGMENTS * ang) / deg); //具体多少个切成多少片
for (let i = 0; i <= segments; i++) {
const p = i / segments;
const t = startAng + (endAng - startAng) * p;
// 核心处理逻辑
const x = x0 + radiusX * Math.cos(t);
const y = y0 + radiusY * Math.sin(t);
ret.push(new Vector2D(x, y));
}
return ret;
}
抛物线代码实现:
// 抛物线
// x = x0 + 2pt^2;
// y = y0 + 2pt;
// console.log(parabolic(0, 0, 100));
function parabolic(
x0: number,
y0: number,
radius,
startAng: number = -Math.PI * 2,
endAng: number = Math.PI * 2
) {
const SEGMENTS = 60; //切分为多少块;
const deg = Math.PI * 2;
const ang = Math.min(deg, endAng - startAng); // 判断是否画整圆
const ret = ang === deg ? [] : [new Vector2D(x0, y0)];
const segments = Math.round((SEGMENTS * ang) / deg); //具体多少个切成多少片
for (let i = 0; i <= segments; i++) {
const p = i / segments;
const t = startAng + (endAng - startAng) * p;
// 核心处理逻辑
const x = x0 + radius * t;
const y = y0 + radius * t * t;
ret.push(new Vector2D(x, y));
}
return ret;
}
实现效果:
3. 曲线代码优化
我们发现上面的代码有很多重复的地方,我们可以用高阶函数的方法对代码进行抽象
3.1 抽象分析
根据分析,我们会发现了如下特点:
-
曲线绘制除了核心的方程式有不同外,其他的绘制方法都是相同的。 -
都是用折线表示曲线,就有边数的概念,我们后面把这个概念统一成片段,英文单词 SEGMENTS (segments)
3.2 基础图形函数
type deleteFirstParameter<T> = T extends (one, ...args: infer R) => any
? R
: any;
interface callBack {
points: Vector2D[];
draw: (...a: deleteFirstParameter<typeof draw>) => void;
}
// 基础图形
function graphics(
fnx,
fny,
SEGMENTS = 60
): (startAng: number, endAng: number, ...args) => callBack {
return function (startAng, endAng, ...args): callBack {
const segments = SEGMENTS;
let points: Vector2D[] = [];
for (let i = 0; i <= segments; i++) {
const p = i / segments;
const t = startAng + (endAng - startAng) * p;
const x = fnx(t, ...args);
const y = fny(t, ...args);
points.push(new Vector2D(x, y));
}
return {
points,
draw: draw.bind(this, points) as (
...a: deleteFirstParameter<typeof draw>
) => void,
};
};
}
我们通过高阶函数的时候,把一段segments通过高阶函数返回,然后再传递给我们的函数,进行处理。
3.3 优化draw方法
let ctxOtions = {
strokeStyle: "black",
fillStyle: "",
close: false,
lineWidth: 0,
};
// 绘制图形
function draw(
points: Vector2D[],
ctx: CanvasRenderingContext2D,
options?: Partial<typeof ctxOtions>
) {
options = options || ctxOtions;
ctx.lineWidth = options.lineWidth;
ctx.strokeStyle = options.strokeStyle;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
options.close && ctx.closePath();
if (options.fillStyle) {
ctx.fillStyle = options.fillStyle;
ctx.fill();
}
ctx.stroke();
}
3.3 重新实现曲线函数
// 抛物线
// x = x0 + 2pt^2;
// y = y0 + 2pt;
function parabolic(
x0: number,
y0: number,
p: number,
startAng: number = -5,
endAng: number = 5
): callBack {
var parabolic = graphics(
(t: number) => x0 + 2 * p * t,
(t: number) => y0 + 2 * p * t * t,
200
);
return parabolic(startAng, endAng);
}
// 椭圆 ellipse
// x = x0 + a*cos(ø)
// y = y0 + b*sin(ø)
function ellipse(
x0: number,
y0: number,
radiusX,
radiusY,
startAng: number = -Math.PI * 2,
endAng: number = Math.PI * 2
) {
var ellipse = graphics(
(t: number) => x0 + radiusX * Math.cos(t),
(t: number) => y0 + radiusY * Math.sin(t),
60
);
return ellipse(startAng, endAng);
}
// 绘制圆
function arc(
...args: [
x0: number,
y0: number,
radius: any,
startAng?: number,
endAng?: number
]
): callBack {
var arc = graphics(
(t: number) => args[0] + args[2] * Math.cos(t),
(t: number) => args[1] + args[2] * Math.sin(t)
);
return arc(args[3] || 0, args[4] || 2 * Math.PI);
}
效果和上面截图是一样的。
4. 其他图形效果
// 心形线
const loveStar = graphics(
(t, a) => a * (2 * Math.sin(t) - Math.sin(2 * t)),
(t, a) => a * (2 * Math.cos(t) - Math.cos(2 * t)),
10000
);
loveStar(-Math.PI, Math.PI, 40).draw(ctx, { strokeStyle: "red" });
// 阿基米德螺旋线
const helical = graphics(
(t, l) => l * t * Math.cos(t),
(t, l) => l * t * Math.sin(t),
10000
);
helical(0, 50, 5).draw(ctx, { strokeStyle: "gray" });
5. 课后习题
完成二次贝塞尔曲线,三次贝塞尔曲线的曲线函数;
5.1 提供曲线方程
二次贝塞尔曲线,P0是起点,P1是控制点,P2是终点三次贝塞尔曲线其中, P和 P是起点和终点,P、P是控制点,所以三阶贝塞尔曲线有两个控制点。