前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >在两条直线相交处添加圆角,算法该如何实现?

在两条直线相交处添加圆角,算法该如何实现?

作者头像
前端西瓜哥
发布于 2024-06-17 05:38:08
发布于 2024-06-17 05:38:08
29101
代码可运行
举报
运行总次数:1
代码可运行

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

下面我们看一个平面几何算法。

已知两条直线形成的折线,和圆角的半径,求在两条直线相交位置添加该圆角后的形状。

如图:

思路

思路非常简单。

将两条直线 往中间位置偏移半径的距离,偏移后的两条直线的 交点就是圆角的圆心

然后基于圆心作两条直线的垂足得到两个点,这两个点就是圆弧起点和终点,然后确定方向就可以了。

Demo 效果演示:

关注公众号,后台回复 “加圆角”,获取在线 demo 地址

实现

我们用两个点表示一条直线。

直线 1 用点 p1、p2 表示,直线 2 用点 p3、p4 表示,圆角半径为 radius。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 const calcRoundCorner = (
  p1: Point,
  p2: Point,
  p3: Point,
  p4: Point,
  radius: number,
) => {
  // ...
}

求偏移直线

我们需要知道两条直线的左右关系,为此我们需要计算两条直线对应向量的叉积。

叉积的作用是判断向量的左右关系。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// p2 到 p1 向量
const v1 = {
  x: p1.x - p2.x,
  y: p1.y - p2.y,
};

// p2 到 p3 的向量
const v2 = {
  x: p4.x - p3.x,
  y: p4.y - p3.y,
};

// 叉积
const cp = v1.x * v2.y - v2.x * v1.y;

注意,这里我们假设坐标系 x 轴向右,y 轴向下。

如果叉积为 0,说明两条直线平行或共线,无法确定圆心位置,没有意义,直接结束返回。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (cp === 0) {
  // 平行,无法生成圆角
  return null;
}

如果叉积小于 0,说明 v2 在 v1 的左边(注意这里的左边指的是向量方向前进方向的左边,不是布局的左边)。

所以中间位置在 v1 的左边,v2 的右边。

v1 对应的直线就需要向左边移动半径距离。

我们求出 v1 的向左法向量,然后让它的模长为半径长度,得到位移向量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 求 v1 向左法向量(这里不是单位向量)
normalVec1 = {
  x: v1.y,
  y: -v1.x,
};

const t1 = radius / distance(p1, p2);
// 算出位移向量
const d = {
  x: normalVec1.x * t1,
  y: normalVec1.y * t1,
};

// line1 沿法向量偏移半径长度
const offsetLine2 = [
  {
    x: p3.x + d2.x,
    y: p3.y + d2.y,
  },
  {
    x: p4.x + d2.x,
    y: p4.y + d2.y,
  },
];

求一个向量的法向量其实就是将该向量旋转 90 度或 -90 度,结果是 x 和 y 交换位置,且其中一个符号取反。

向左的法向量对应的旋转 -90度,这里可以考虑引入矩阵库数学工具,使用旋转矩阵提高代码的可读性。

同理,v2 对应的直线就需要向右移动半径距离,这里不再赘述。

如果叉积大于 0,说明 v2 在 v1 的右边,和前面的区别就是法向量反过来,其它都是一样的。

求圆心

前面我们得到了偏移后的两条直线,就可以用解方程的方式求两条直线的圆心了。

这个我之前的文章讲过,这里直接给求两直线交点的代码实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 求两条直线交点
 */
export const getLineIntersection = (
  p1: Point,
  p2: Point,
  p3: Point,
  p4: Point,
): Point | null => {
  const { x: x1, y: y1 } = p1;
  const { x: x2, y: y2 } = p2;
  const { x: x3, y: y3 } = p3;
  const { x: x4, y: y4 } = p4;

  const a = y2 - y1;
  const b = x1 - x2;
  const c = x1 * y2 - x2 * y1;

  const d = y4 - y3;
  const e = x3 - x4;
  const f = x3 * y4 - x4 * y3;

  // 计算分母
  const denominator = a * e - b * d;

  // 判断分母是否为 0(代表平行)
  if (Math.abs(denominator) < 0.000000001) {
    // 这里有个特殊的重叠但只有一个交点的情况,可以考虑处理一下
    return null;
  }

  const px = (c * e - f * b) / denominator;
  const py = (a * f - c * d) / denominator;

  return { x: px, y: py };
};

所以圆角的圆心为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 求偏移后两条直线的交点,这个交点就是圆心
const circleCenter = getLineIntersection(
  offsetLine1[0],
  offsetLine1[1],
  offsetLine2[0],
  offsetLine2[1],
);

求垂足

然后我们将圆心往两条直线上投影,求垂足点,这两个点是圆弧的起点和终点。

这个投影,或者说找到直线的最近点算法,我之前的文章也讲过,这里也直接贴代码实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const closestPointOnLine = (
  p1: Point,
  p2: Point,
  p: Point,
  /** 是否限制在在线段之内 */
  canOutside = false,
) => {
  if (p1.x === p2.x && p1.y === p2.y) {
    return {
      t: 0,
      d: distance(p1, p),
      point: { x: p1.x, y: p1.y },
    };
  }
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  let t = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy);
  if (!canOutside) {
    t = Math.max(0, Math.min(1, t));
  }
  const closestPt = {
    x: p1.x + t * dx,
    y: p1.y + t * dy,
  };
  return {
    t,
    d: distance(p, closestPt),
    point: closestPt,
  };
};

求出圆弧起点和终点:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 求圆心到两条线的垂足
const { point: start } = closestPointOnLine(p1, p2, circleCenter, true);
const { point: end } = closestPointOnLine(p3, p4, circleCenter, true);

然后就是圆弧反向,基于叉积的正负值可得出。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const angleDir = cp < 0, // 正值 -> 顺时针

确定圆弧和收尾工作

至此我们知道了 圆心、半径、起点、终点、方向,圆弧就能确定了

后续我们只需要将这些圆弧的信息转换为渲染引擎支持的数据结构常见的有三种

最后可能要调整一下线段的端点位置,使其落在圆弧端点上。

扩展点

有几个扩展点。

首先是对于 圆角半径大小的限制 的考虑。

一般情况下,圆角圆弧的端点不会超出两条线段的范围。

但特殊情况下还是会超出的:设置一个很大的圆角半径。

AutoCAD 的做法是,提示 “圆角半径太大”,不允许生成。

Figma 的做法是,会使用圆角效果,但实际渲染时的 radius 不能超出某个值,保证圆弧的端点不超出线段区间。

不管哪种方案,都要求一下两条线段各自能支持的最大圆角半径,取其中较小的,作为阈值。

可以用点积求出夹角,然后用三角函数求出支持最大圆角半径:

曲线也能做相交处圆角,原理还是一样的,曲线同样也是向中间位置偏移一段距离,接着求圆角中点,然后就是求到两条线的垂足。

完整代码实现

因为还带上了一些子算法,所以代码有一点点长。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 求两直线的圆角圆弧
const calcRoundCorner = (
  p1: Point,
  p2: Point,
  p3: Point,
  p4: Point,
  radius: number,
) => {
  // p2 到 p1 向量
  const v1 = {
    x: p1.x - p2.x,
    y: p1.y - p2.y,
  };
  // p2 到 p3 的向量
  const v2 = {
    x: p4.x - p3.x,
    y: p4.y - p3.y,
  };
  // 求叉积
  const cp = v1.x * v2.y - v2.x * v1.y;
  if (cp === 0) {
    // 平行,无法生成圆角
    return null;
  }
  let normalVec1: Point;
  let normalVec2: Point;
  // v2 在 v1 的左边
  if (cp < 0) {
    // 求 v1 向左法向量
    normalVec1 = {
      x: v1.y,
      y: -v1.x,
    };
    // 求 v2 向右法向量
    normalVec2 = {
      x: -v2.y,
      y: v2.x,
    };
  }
  // v2 在 v1 的右边
  else {
    normalVec1 = {
      x: -v1.y,
      y: v1.x,
    };
    normalVec2 = {
      x: v2.y,
      y: -v2.x,
    };
  }

  // 求沿法向量偏移半径长度的 line1
  const t1 = radius / distance(p1, p2);
  const d = {
    x: normalVec1.x * t1,
    y: normalVec1.y * t1,
  };
  const offsetLine1 = [
    {
      x: p1.x + d.x,
      y: p1.y + d.y,
    },
    {
      x: p2.x + d.x,
      y: p2.y + d.y,
    },
  ];

  // 求沿法向量偏移半径长度的 line1
  const t2 = radius / distance(p3, p4);
  const d2 = {
    x: normalVec2.x * t2,
    y: normalVec2.y * t2,
  };
  const offsetLine2 = [
    {
      x: p3.x + d2.x,
      y: p3.y + d2.y,
    },
    {
      x: p4.x + d2.x,
      y: p4.y + d2.y,
    },
  ];

  // 求偏移后两条直线的交点,这个交点就是圆心
  const circleCenter = getLineIntersection(
    offsetLine1[0],
    offsetLine1[1],
    offsetLine2[0],
    offsetLine2[1],
  )!;

  // 求圆心到两条线的垂足
  const { point: start } = closestPointOnLine(p1, p2, circleCenter, true);
  const { point: end } = closestPointOnLine(p3, p4, circleCenter, true);

  // 圆心到垂足的弧度
  const angleBase = { x: 1, y: 0 };
  const startAngle = getSweepAngle(angleBase, {
    x: start.x - circleCenter.x,
    y: start.y - circleCenter.y,
  });
  const endAngle = getSweepAngle(angleBase, {
    x: end.x - circleCenter.x,
    y: end.y - circleCenter.y,
  });

  return {
    offsetLine1,
    offsetLine2,
    circleCenter,
    start,
    end,
    startAngle,
    endAngle,
    angleDir: cp < 0, // 正 -> 顺时针
  };
};

// 求两点距离
const distance = (p1: Point, p2: Point) => {
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  return Math.sqrt(dx * dx + dy * dy);
};

// 求两直线交点
const getLineIntersection = (
  p1: Point,
  p2: Point,
  p3: Point,
  p4: Point,
): Point | null => {
  const { x: x1, y: y1 } = p1;
  const { x: x2, y: y2 } = p2;
  const { x: x3, y: y3 } = p3;
  const { x: x4, y: y4 } = p4;

  const a = y2 - y1;
  const b = x1 - x2;
  const c = x1 * y2 - x2 * y1;

  const d = y4 - y3;
  const e = x3 - x4;
  const f = x3 * y4 - x4 * y3;

  // 计算分母
  const denominator = a * e - b * d;

  // 判断分母是否为 0(代表平行)
  if (Math.abs(denominator) < 0.000000001) {
    // 这里有个特殊的重叠但只有一个交点的情况,可以考虑处理一下
    return null;
  }

  const px = (c * e - f * b) / denominator;
  const py = (a * f - c * d) / denominator;

  return { x: px, y: py };
};

// 到直线的最近点(或投影)
const closestPointOnLine = (
  p1: Point,
  p2: Point,
  p: Point,
  /** 是否限制在在线段之内 */
  canOutside = false,
) => {
  if (p1.x === p2.x && p1.y === p2.y) {
    return {
      t: 0,
      d: distance(p1, p),
      point: { x: p1.x, y: p1.y },
    };
  }
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  let t = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy);
  if (!canOutside) {
    t = Math.max(0, Math.min(1, t));
  }
  const closestPt = {
    x: p1.x + t * dx,
    y: p1.y + t * dy,
  };
  return {
    t,
    d: distance(p, closestPt),
    point: closestPt,
  };
};


/**
 * 求向量 a 到向量 b 扫过的夹角
 * 这里假设为 x时针方向为正
 */
export const getSweepAngle = (a: Point, b: Point) => {
  // 使用点乘求夹角
  const dot = a.x * b.x + a.y * b.y;
  const d = Math.sqrt(a.x * a.x + a.y * a.y) * Math.sqrt(b.x * b.x + b.y * b.y);
  let cosTheta = dot / d;
  // 修正精度问题导致的 cosTheta 超出 [-1, 1] 的范围
  // 导致 Math.acos(cosTheta) 的结果为 NaN
  if (cosTheta > 1) {
    cosTheta = 1;
  } else if (cosTheta < -1) {
    cosTheta = -1;
  }

  let theta = Math.acos(cosTheta);
  // 通过叉积判断方向
  // 如果 b 在 a 的左边,则取负值
  if (a.x * b.y - a.y * b.x < 0) {
    theta = -theta;
  }

  return theta;
};

结尾

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

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
平面几何:求直线线段的轮廓线
求线段的法向量,乘以线宽的一半,得到位移向量。然后让线段的两个点分别做两个方向的位移,得到多边形的 4 个顶点,将它们按照一定顺序连接起来得到多边形,这个多边形就是我们要求的轮廓多边形。
前端西瓜哥
2024/07/31
1460
平面几何:求直线线段的轮廓线
平面几何算法:求点到直线和圆的最近点
比如精细的图形拾取(尤其是一些没有填充只有描边的图形)。如果光标点到最近点的距离小于某个阈值,计算图形就算被选中。
前端西瓜哥
2024/03/04
4060
平面几何算法:求点到直线和圆的最近点
【POJ 3525】Most Distant Point from the Sea(直线平移、半平面交)
二分这个半径,将所有直线向多边形中心平移r距离,如果半平面交不存在那么r大了,否则r小了。
饶文津
2020/06/02
3060
Java 通过向量,计算移动方向,计算线段角度等
向量是指在数学中用于表示大小和方向的量。在计算机科学中,向量通常用于表示物体的位置、速度和加速度等。在Java中,可以使用坐标系中两点之间的差异来计算向量之间的距离。
zinyan.com
2023/07/14
9280
Java 通过向量,计算移动方向,计算线段角度等
ACM刷题之路(六)直线相交问题 POJ3304 Segments
Given n segments in the two dimensional space, write a program, which determines if there exists a line such that after projecting these segments on it, all projected segments have at least one point in common.
Designer 小郑
2023/07/31
1740
ACM刷题之路(六)直线相交问题 POJ3304 Segments
位置和方向的世界,计算几何的基本问题
本文从最基本的线段相交问题出发,从解析几何进入计算几何,介绍点积和叉积这个最基本的计算几何工具,引入计算几何这个关于位置和方向的大航海世界~
ACM算法日常
2020/06/29
9700
二维几何基础
1. 点、线、凸边形 /******************************************************* 二维几何基础 【注意】数组下标从1开始。 *******************************************************/ #include <iostream> #include <cstdio> #include <cstdlib> #include <cmath> #include <ioma
hotarugali
2022/03/01
6580
代数几何:点,线,抛物线,圆,球,弧度和角度
一, 笛卡尔坐标系                         笛卡尔坐标系是数学中的坐标系,而计算机中则采用屏幕坐标系统. 而三维坐标系则没有一个工业标准,分别有 Y轴向上(y-up)的坐标系,
^_^肥仔John
2018/01/18
1.2K0
代数几何:点,线,抛物线,圆,球,弧度和角度
计算几何之求两线段的交点
思路就是连接线段的端点,构造向量,从而构造出相似三角形,然后求出交点在一条线段上的位置(用比例t来表示),然后再加到线段端点上就可以了。
灯珑LoGin
2022/10/31
1K0
电信网络拓扑图自动布局之总线
在前面《电信网络拓扑图自动布局》一文中,我们大体介绍了 HT for Web 电信网络拓扑图自动布局的相关知识,但是都没有深入地描述各种自动布局的用法,我们今天在这边就重点介绍总线的具体实现方案。 在
HT for Web
2018/01/03
1.2K0
电信网络拓扑图自动布局之总线
计算机图形学(OpenGL版)书中其它代码
本处代码主要为各章中除章节末的编程实例之外的有关代码,现全部贴出以飨读者。 第3章 二维图形生成 3.1 直线生成算法 3.1.1 数值微分法 void LineDDA(int x1, int y1, int x2, int y2, int color) { int dm=0; if (abs(x2-x1)>= abs(y2-y1) //abs是求绝对值的函数 dm=abs(x2-x1); //x为计长方向 else dm=abs(y2-
步行者08
2018/10/09
1.7K0
mfc vc++ 如何求点到直线的距离 判断点是否在线要素上?
首先知道线要素由点要素数组points构成,points可以是CPoint类型、Point类型、或者自定义类型。要判断Point类型的点p是否在由points组成的线要素上,只需要遍历计算该点到每一条线的距离,来判断点是否在线要素的某一部分上。
acoolgiser
2019/01/16
1.1K0
吸附设计:学会正确地贴贴
本文将介绍图形编辑器中吸附系统中,各种吸附类型的吸附逻辑和算法实现,让大家对吸附有一个概念。
前端西瓜哥
2024/07/12
2200
吸附设计:学会正确地贴贴
计算几何之线段相交问题(平面扫描)
求n条线段的交点,可以用抽选配对的方式来遍历所有的情况,这样子时间复杂度为O(n2).
灯珑LoGin
2022/10/31
1.1K0
计算几何之线段相交问题(平面扫描)
几何算法:判断两条线段是否相交
一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)),两条线段是两个两点式,这样就是 二元一次方程组 了 ,就能求出两条直线的交点。
前端西瓜哥
2023/08/18
1K0
几何算法:判断两条线段是否相交
解析几何:计算两条线段的交点
我们要实现 getLineSegIntersection 方法:提供两条线段,计算它们的交点。
前端西瓜哥
2023/10/22
5040
解析几何:计算两条线段的交点
ZOJ 3728 Collision
http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5074 题意:两个圆,小圆为实体,具有碰撞性。其中一个内含于另外一个,另有一枚硬
用户1624346
2018/04/17
5960
丘比特的箭(点是否在面内)- HDU 1756
对于点A是否在多边形P内的判定, 一般有两种方法:射线法和转角法。 这里介绍一下射线法。
ACM算法日常
2018/08/07
1K0
丘比特的箭(点是否在面内)- HDU 1756
【算法】Graham 凸包扫描算法 ( 凸包概念 | 常用的凸包算法 | 角排序 | 叉积 | Python 代码示例 )
博客代码下载 : https://download.csdn.net/download/han1202012/89428182
韩曙亮
2024/06/14
6010
【算法】Graham 凸包扫描算法 ( 凸包概念 | 常用的凸包算法 | 角排序 | 叉积 | Python 代码示例 )
2018 Wannafly summer camp Day2--New Game!
New Game! 描述 题目描述: Eagle Jump公司正在开发一款新的游戏。泷本一二三作为其员工,获得了提前试玩的机会。现在她正在试图通过一个迷宫。
Enterprise_
2019/02/20
3470
相关推荐
平面几何:求直线线段的轮廓线
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验