前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >平面几何:求直线线段的轮廓线

平面几何:求直线线段的轮廓线

作者头像
前端西瓜哥
发布2024-07-31 19:59:24
680
发布2024-07-31 19:59:24
举报
文章被收录于专栏:前端西瓜哥的前端文章

大家好,我是前端西瓜哥。

今天我们来学习简单的平面几何算法,求直线线段的轮廓线。

需求是给两个点表达的直线线段,以及线宽,求它的轮廓线多边形

对于直线线段,末端有三种样式:

  1. Butt:平端,不增加额外形状;
  2. Square:方形端,额外补充一个矩形,宽为线宽,高为线宽的一半;
  3. Round:圆形端,额外补充一个半圆,半径为线宽的一半。

本文实现算法的在线交互 Demo:

https://codepen.io/F-star/pen/PorzxLw

Butt(平端)

我们先看看平端的实现。

求线段的法向量,乘以线宽的一半,得到位移向量。然后让线段的两个点分别做两个方向的位移,得到多边形的 4 个顶点,将它们按照一定顺序连接起来得到多边形,这个多边形就是我们要求的轮廓多边形。

求法向量,其实就是计算向量 p1-p2 旋转 90 度。旋转的方向没关系,计算出的法向量有两个方向,都可以,只要点的顺序。

将一个向量旋转 90 度,可以用三角函数推导,或者直接用旋转矩阵,具体推导就不做了。

有个特殊的规律:对于向量旋转 90 度的向量,我们只需要把 x 和 y 交换位置,然后将其中一个值取反。

代码语言:javascript
复制
x2 = y;
y2 = -x;

或者你可以点积的角度看,互相垂直的两条向量的点积总是零

代码语言:javascript
复制
// 法向量,模长为线段长度
const tan = {
  x: p2.x - p1.x,
  y: p2.y - p1.y,
};
// 线性插值比率 t
const t = width / 2 / Math.sqrt(tan.x * tan.x + tan.y * tan.y);
// 求法向量,模长为 width /2
const normal = {
  x: tan.y * t,
  y: -tan.x * t,
};

最后依次算出 4 个顶点。

代码语言:javascript
复制
const vertexes = [
  { x: p1.x + normal.x, y: p1.y + normal.y },
  { x: p2.x + normal.x, y: p2.y + normal.y },
  { x: p2.x - normal.x, y: p2.y - normal.y },
  { x: p1.x - normal.x, y: p1.y - normal.y },
];

完整代码:

代码语言:javascript
复制
const outlineLineWithButtCap = (p1: Point, p2: Point, width: number) => {
  const tan = {
    x: p2.x - p1.x,
    y: p2.y - p1.y,
  };
  const t = width / 2 / Math.sqrt(tan.x * tan.x + tan.y * tan.y);
  // 求法向量,模长为 width /2
  const normal = {
    x: tan.y * t,
    y: -tan.x * t,
  };

  const vertexes = [
    { x: p1.x + normal.x, y: p1.y + normal.y },
    { x: p2.x + normal.x, y: p2.y + normal.y },
    { x: p2.x - normal.x, y: p2.y - normal.y },
    { x: p1.x - normal.x, y: p1.y - normal.y },
  ];

  return vertexes;
};

看看渲染效果,很完美。

Square(方形端)

Square 方形端,需要额外补充一个矩形,宽为线宽,高为线宽的一半。

观察就能发现,Square 等价于让直线两端往两测延长 “线宽一半” 的长度,然后应用 butt 的算法。

代码语言:javascript
复制
const outlineLineWithSquareCap = (
  p1: Point,
  p2: Point,
  width: number,
) => {
  const tan = {
    x: p2.x - p1.x,
    y: p2.y - p1.y,
  };
  const t = width / 2 / Math.sqrt(tan.x * tan.x + tan.y * tan.y);

  // 需要位移的距离
  const dx = tan.x * t;
  const dy = tan.y * t;

  p2 = {
    x: p2.x + dx,
    y: p2.y + dy,
  };

  p1 = {
    x: p1.x - dx,
    y: p1.y - dy,
  };

  // 使用 butt 的算法
  return outlineLineWithButtCap(p1, p2, width);
};

渲染结果:

Round(圆形端)

最后是圆形端,需要给末端额外加半圆,圆半径为线宽的一半。

我们要求的是多边形,其实也就是在 butt 求出的 4 个顶点的基础上,再插入两个圆弧。

其实圆弧很容易确定,我们已经知道每个圆弧的两个端点,还有半径。

但麻烦的点在于我们需要用某种方式表达这个圆弧,圆弧的表达有好几种,且有点复杂,不同渲染引擎支持的圆弧表达是不一样的,这代表我们可能要在多种表达中进行转换。()

常见的圆弧表达有三种:

  1. 圆心、半径 、起始角、结束角、方向;
  2. 起点、终点、半径、优弧、方向;
  3. 起点、终点、凸度;

这三种表达我在之前的文章详细讲解过,感兴趣可以 前往阅读

我们需要在里面选择一种。

这段圆弧是作为多段线的一部分,用带有起点、终点的表达会更好些,再考虑到能够无缝使用 SVG 的 Path 元素表达,最终我们选择用第二种方案:起点、终点、半径、优弧(largeArc)、方向(sweep)

起点、终点、半径我们都已经有了,我们需要确定优弧(是否使用大的弧)和方向。

因为是半圆,所以优弧是 true 还是 false 并无所谓,它们对应的两个圆会重叠为一个圆,这里我们取 true。

虽然在计算 butt 时,法向量的方向无关紧要,但对于 round 末端效果还是有影响的。y 取反的法向量,对应的多边形的方向是顺时针,圆弧自然也需要是顺时针,所以方向(sweep)为 true。

完整代码实现:

代码语言:javascript
复制
const outlineLineWithRoundCap = (
  p1: Point,
  p2: Point,
  width: number,
) => {
  const buttOutline = outlineLineWithButtCap(p1, p2, width);

  return [
    buttOutline[0],
    buttOutline[1],
    {
      ...buttOutline[2],
      radius: width / 2,
      largeArc: true,
      sweep: true,
    },
    buttOutline[3],
    {
      ...buttOutline[0],
      radius: width / 2,
      largeArc: true,
      sweep: true,
    },
  ];
};

SVG 的 Path 元素的 d 属性值为:

代码语言:javascript
复制
// 起点
let d = `M ${outline[0].x} ${outline[0].y} `;

for (let i = 1; i < outline.length; i++) {
  const seg = outline[i];
  if ('radius' in seg) {
    // 圆弧
    d += `A ${seg.radius} ${seg.radius} 0 ${seg.largeArc ? 1 : 0} ${
      seg.sweep ? 1 : 0
    } ${seg.x} ${seg.y} `;
  } else {
    // 直线
    d += `L ${seg.x} ${seg.y} `;
  }
}

// 闭合
d += 'Z';

渲染结果:

三种效果对比

我们将这三种算法得到的多边形同时渲染,对比一下效果。

结尾

这次的算法还是挺简单的,总结一下,就是 求法向量,把直线的两个端点往两侧位移一下,得到一个矩形多边形,然后根据末端样式,给两边补上矩形或半圆

末端样式是可以做自定义扩展,补上任意你想要的图形的。

比如我给某一端补上一个三角形,就变成了什么?变成了一个箭头线。

我是前端西瓜哥,关注我,学习更多平面几何知识。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端西瓜哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Butt(平端)
  • Square(方形端)
  • Round(圆形端)
  • 三种效果对比
  • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档