vlambda博客
学习文章列表

3. webGL 可视化学习 —— 绘制几何图形

这章我们来学习绘制几何图形,主要包括正多边形和曲线的绘制;

1. 绘制正多边形

分别绘制三角形,正四边形,正六形,正二十六边

image.png

绘制思路

我们采用向量的方式进行绘制,其代码如下:

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(-2560), new Vector2D(2560)]); 
// 绘制y轴
draw([new Vector2D(0-256), new Vector2D(0256)]);
//绘制三边形
draw(regularShape(315050100));
//绘制四边形
draw(regularShape(4-5050100));
//绘制六边形
draw(regularShape(6-100-15050));
//绘制二十五边形
draw(regularShape(25128-15815));

特点

根据观察,我们会发现正多边形的边数越多,它就会越来越接近一个圆,我们用极限的思想,就可以得出一个结论,主要我们绘制的边数足够多,它看起来就会像一个圆。

缺点

  1. 定义边数、起点、一条边的长度,这就和我们通常的使用习惯,也就是定义边数、中心和半径不符。
  2. 如果我们按照现在这种定义方式绘图,是很难精确对应到图形的位置和大小的。
  3. regularShape 可以绘制正几何图形。但是对于椭圆、抛物线、贝塞尔曲线等其他曲线的绘制就没法实现了。

2. 绘制曲线

绘制曲线我们采用公式法,因为很多曲线是有曲线公式的

常见方程式

圆的方程:3. webGL 可视化学习 —— 绘制几何图形

椭圆的方程:3. webGL 可视化学习 —— 绘制几何图形

抛物线的方程:3. webGL 可视化学习 —— 绘制几何图形

代码实现:

圆的代码实现:

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. webGL 可视化学习 —— 绘制几何图形
image.png

3. 曲线代码优化

我们发现上面的代码有很多重复的地方,我们可以用高阶函数的方法对代码进行抽象

3.1 抽象分析

根据分析,我们会发现了如下特点:

  1. 曲线绘制除了核心的方程式有不同外,其他的绘制方法都是相同的。
  2. 都是用折线表示曲线,就有边数的概念,我们后面把这个概念统一成片段,英文单词 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. 其他图形效果

3. webGL 可视化学习 —— 绘制几何图形
image.png
// 心形线
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(0505).draw(ctx, { strokeStyle: "gray" });

5. 课后习题

完成二次贝塞尔曲线,三次贝塞尔曲线的曲线函数;

5.1 提供曲线方程

二次贝塞尔曲线,P0是起点,P1是控制点,P2是终点三次贝塞尔曲线其中, P和 P是起点和终点,P、P是控制点,所以三阶贝塞尔曲线有两个控制点。

致谢