0. 你有一个角落:
1. 你知道角点的坐标,假设它是P1,P2和P:
2. 现在你可以从点得到向量和向量之间的角度:
angle = atan(PY - P1Y, PX - P1X) - atan(PY - P2Y, PX - P2X)
3. 获取角点与圆相交点之间线段的长度。
segment = PC1 = PC2 = 半径 / |tan(角度 / 2)|
4. 在这里,您需要检查线段的长度和PP1和PP2的最小长度:
PP1的长度:
PP1 = sqrt((PX - P1X)2 + (PY - P1Y)2)
PP2的长度:
PP2 = sqrt((PX - P2X)2 + (PY - P2Y)2)如果线段 > PP1 或线段 > PP2,则需要减小半径:
min = Min(PP1, PP2)(对于多边形最好将此值除以2) segment > min ? segment = min radius = segment * |tan(angle / 2)|5. 获取PO的长度:
PO = sqrt(radius2 + segment2)6. 通过向量坐标、向量长度和线段长度之间的比例来获取C1X和C1Y:
(PX - C1X) / (PX - P1X) = PC1 / PP1
所以:
C1X = PX - (PX - P1X) * PC1 / PP1
对于C1Y,同样的方法:
C1Y = PY - (PY - P1Y) * PC1 / PP1
7. 用同样的方式获取C2X和C2Y:
C2X = PX - (PX - P2X) * PC2 / PP2 C2Y = PY - (PY - P2Y) * PC2 / PP2
8.现在,您可以通过比例以相同的方式使用向量PC1和PC2的加法来找到圆的中心:
(PX - OX) / (PX - CX) = PO / PC (PY - OY) / (PY - CY) = PO / PC
这里:
CX = C1X + C2X - PX CY = C1Y + C2Y - PY PC = sqrt((PX - CX)2 + (PY - CY)2)
定义:
dx = PX - CX = PX * 2 - C1X - C2X dy = PY - CY = PY * 2 - C1Y - C2Y
因此:
PC = 根号下(dx的平方 + dy的平方)
OX = PX - dx * PO / PC OY = PY - dy * PO / PC
9. 在这里,您可以绘制一个弧线。为此,您需要获取弧线的起始角度和结束角度:
在这里找到:
startAngle = atan((C1Y - OY) / (C1X - OX)) endAngle = atan((C2Y - OY) / (C2X - OX))最后,您需要获得一个扫描角度并进行一些检查:
sweepAngle = endAngle - startAngle
sweepAngle < 0 ?
sweepAngle = - sweepAngle
startAngle = endAngle
检查扫描角度是否大于180度:
sweepAngle > 180 ?
sweepAngle = 180 - sweepAngle
11. 现在你可以绘制圆角:
private void DrawRoundedCorner(Graphics graphics, PointF angularPoint,
PointF p1, PointF p2, float radius)
{
//Vector 1
double dx1 = angularPoint.X - p1.X;
double dy1 = angularPoint.Y - p1.Y;
//Vector 2
double dx2 = angularPoint.X - p2.X;
double dy2 = angularPoint.Y - p2.Y;
//Angle between vector 1 and vector 2 divided by 2
double angle = (Math.Atan2(dy1, dx1) - Math.Atan2(dy2, dx2)) / 2;
// The length of segment between angular point and the
// points of intersection with the circle of a given radius
double tan = Math.Abs(Math.Tan(angle));
double segment = radius / tan;
//Check the segment
double length1 = GetLength(dx1, dy1);
double length2 = GetLength(dx2, dy2);
double length = Math.Min(length1, length2);
if (segment > length)
{
segment = length;
radius = (float)(length * tan);
}
// Points of intersection are calculated by the proportion between
// the coordinates of the vector, length of vector and the length of the segment.
var p1Cross = GetProportionPoint(angularPoint, segment, length1, dx1, dy1);
var p2Cross = GetProportionPoint(angularPoint, segment, length2, dx2, dy2);
// Calculation of the coordinates of the circle
// center by the addition of angular vectors.
double dx = angularPoint.X * 2 - p1Cross.X - p2Cross.X;
double dy = angularPoint.Y * 2 - p1Cross.Y - p2Cross.Y;
double L = GetLength(dx, dy);
double d = GetLength(segment, radius);
var circlePoint = GetProportionPoint(angularPoint, d, L, dx, dy);
//StartAngle and EndAngle of arc
var startAngle = Math.Atan2(p1Cross.Y - circlePoint.Y, p1Cross.X - circlePoint.X);
var endAngle = Math.Atan2(p2Cross.Y - circlePoint.Y, p2Cross.X - circlePoint.X);
//Sweep angle
var sweepAngle = endAngle - startAngle;
//Some additional checks
if (sweepAngle < 0)
{
startAngle = endAngle;
sweepAngle = -sweepAngle;
}
if (sweepAngle > Math.PI)
sweepAngle = Math.PI - sweepAngle;
//Draw result using graphics
var pen = new Pen(Color.Black);
graphics.Clear(Color.White);
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.DrawLine(pen, p1, p1Cross);
graphics.DrawLine(pen, p2, p2Cross);
var left = circlePoint.X - radius;
var top = circlePoint.Y - radius;
var diameter = 2 * radius;
var degreeFactor = 180 / Math.PI;
graphics.DrawArc(pen, left, top, diameter, diameter,
(float)(startAngle * degreeFactor),
(float)(sweepAngle * degreeFactor));
}
private double GetLength(double dx, double dy)
{
return Math.Sqrt(dx * dx + dy * dy);
}
private PointF GetProportionPoint(PointF point, double segment,
double length, double dx, double dy)
{
double factor = segment / length;
return new PointF((float)(point.X - dx * factor),
(float)(point.Y - dy * factor));
}
要获取弧的点,您可以使用以下代码:
//One point for each degree. But in some cases it will be necessary
// to use more points. Just change a degreeFactor.
int pointsCount = (int)Math.Abs(sweepAngle * degreeFactor);
int sign = Math.Sign(sweepAngle);
PointF[] points = new PointF[pointsCount];
for (int i = 0; i < pointsCount; ++i)
{
var pointX =
(float)(circlePoint.X
+ Math.Cos(startAngle + sign * (double)i / degreeFactor)
* radius);
var pointY =
(float)(circlePoint.Y
+ Math.Sin(startAngle + sign * (double)i / degreeFactor)
* radius);
points[i] = new PointF(pointX, pointY);
}
PointF[] points = new PointF[pointsCount];
for(int i=0; i
- ZouBiif (sweepAngle > Math.PI) sweepAngle = Math.PI - sweepAngle;
为 if (sweepAngle > Math.PI) sweepAngle = -(2 * Math.PI - sweepAngle);
来解决曲线缺失的问题。请注意,这两行代码的含义不变,只是数学计算方式略有不同。 - Martin。其中
ax = C1x - Px,
bx = C2x - Px,
ax + bx = Cx - Px=>
Cx - Px = C1x - Px + C2x - Px=>
Cx = C1x + C2x - Px`。 - nempoBu4我手头没有合适的绘图软件,但这张图大致展示了这个想法:
此时,您需要引入类来表示由线段和弧段组成的图形,或将弧段多边化为适当的精度并将所有段添加到多边形中。
更新:我已更新图像,标记点P1、P2和P3以及法向量Norm12和Norm23。归一化的法向量仅在翻转方向时是唯一的,并且您应按以下方式选择翻转:
Norm12与(P3-P2)的点积必须为正数。如果它为负,则将Norm12乘以-1.0。如果为零,则点共线,不需要创建圆角。这是因为您想向P3偏移。
Norm23与(P1-P2)的点积也必须为正数,因为您正在向P1偏移。
这是适用于Objective-C的nempoBu4的解答:
typedef enum {
path_move_to,
path_line_to
} Path_command;
static inline CGFloat sqr (CGFloat a) {
return a * a;
}
static inline CGFloat positive_angle (CGFloat angle) {
return angle < 0 ? angle + 2 * (CGFloat) M_PI : angle;
}
static void add_corner (UIBezierPath* path, CGPoint p1, CGPoint p, CGPoint p2, CGFloat radius, Path_command first_add) {
// 2
CGFloat angle = positive_angle (atan2f (p.y - p1.y, p.x - p1.x) - atan2f (p.y - p2.y, p.x - p2.x));
// 3
CGFloat segment = radius / fabsf (tanf (angle / 2));
CGFloat p_c1 = segment;
CGFloat p_c2 = segment;
// 4
CGFloat p_p1 = sqrtf (sqr (p.x - p1.x) + sqr (p.y - p1.y));
CGFloat p_p2 = sqrtf (sqr (p.x - p2.x) + sqr (p.y - p2.y));
CGFloat min = MIN(p_p1, p_p2);
if (segment > min) {
segment = min;
radius = segment * fabsf (tanf (angle / 2));
}
// 5
CGFloat p_o = sqrtf (sqr (radius) + sqr (segment));
// 6
CGPoint c1;
c1.x = (CGFloat) (p.x - (p.x - p1.x) * p_c1 / p_p1);
c1.y = (CGFloat) (p.y - (p.y - p1.y) * p_c1 / p_p1);
// 7
CGPoint c2;
c2.x = (CGFloat) (p.x - (p.x - p2.x) * p_c2 / p_p2);
c2.y = (CGFloat) (p.y - (p.y - p2.y) * p_c2 / p_p2);
// 8
CGFloat dx = p.x * 2 - c1.x - c2.x;
CGFloat dy = p.y * 2 - c1.y - c2.y;
CGFloat p_c = sqrtf (sqr (dx) + sqr (dy));
CGPoint o;
o.x = p.x - dx * p_o / p_c;
o.y = p.y - dy * p_o / p_c;
// 9
CGFloat start_angle = positive_angle (atan2f ((c1.y - o.y), (c1.x - o.x)));
CGFloat end_angle = positive_angle (atan2f ((c2.y - o.y), (c2.x - o.x)));
if (first_add == path_move_to) {
[path moveToPoint: c1];
}
else {
[path addLineToPoint: c1];
}
[path addArcWithCenter: o radius: radius startAngle: start_angle endAngle: end_angle clockwise: angle < M_PI];
}
UIBezierPath* path_with_rounded_corners (NSArray<NSValue*>* points, CGFloat corner_radius) {
UIBezierPath* path = [UIBezierPath bezierPath];
NSUInteger count = points.count;
for (NSUInteger i = 0; i < count; ++i) {
CGPoint prev = points[i > 0 ? i - 1 : count - 1].CGPointValue;
CGPoint p = points[i].CGPointValue;
CGPoint next = points[i + 1 < count ? i + 1 : 0].CGPointValue;
add_corner (path, prev, p, next, corner_radius, i == 0 ? path_move_to : path_line_to);
}
[path closePath];
return path;
}
我可以提供一种简单、可计算和可编程的方法,该方法只使用了最少的计算,包括“仅”3个平方根和没有任何反三角函数。
请不要因为下面故意详细的解释而感到畏惧,我写成这样是为了确保这种绝对微不足道(与此同时其他所有解决方案在此撰写时都要复杂得多)算法能够被理解。实际上,在承认替代方案所需的算法和计算复杂性(其中包括多次调用反三角函数(它们隐藏了许多计算复杂性在它们貌似无害的名称背后)以及更大量的操作)之后,我才想出这个算法。
(我已经通过JavaScript和SVG编程验证了所提出的方法的有效性。我将使用前者来帮助说明这种方法)
假设你想要“圆角”的某个拐角由已知点A、B和C组成,其中B是“拐角”。
该解决方案可以通过以下步骤描述:
计算 BF 向量的长度:
长度等于你选择的圆的半径(FO)除以向量 BF 和 BO 之间夹角的正切值。这是因为由点 B、O 和 F 组成的三角形是一个“直角”三角形(向量 BF 和 FO 之间的夹角为 90 度)。
向量 BF 和 BO 之间的夹角是向量 BA 和 BC 之间夹角的一半。这可能听起来很明显,但可以肯定地证明它是微不足道的,但我省略了证明。
角度之间的关系很有用,因为恰好存在一个相当简单的方程式,表达了角度的正切和两倍角度的余弦之间的关系:Math.tan(a/2) == Math.sqrt((1 - Math.cos(a)) / (1 + Math.cos(a)))
。
恰好,向量 BA 和 BC 之间的夹角的余弦(Math.cos(a)
)是两个向量的点积除以它们长度的乘积(请参见 Wikipedia 上向量点积的定义)。
因此,计算出角度的余弦值后,您可以计算出半角度的正切值,随后计算出 BF 的长度:
(图例:我将向量(BA
、BC
等)建模为具有属性 x
和 y
的对象,用于屏幕空间中的相应坐标(X 向右增加,Y 向下增加);radius
是所需圆角的半径,BF_length
是 BF 的长度(显然))
/// 辅助函数
const length = v => Math.sqrt(v.x * v.x + v.y * v.y);
const dot_product = (v1, v2) => v1.x * v2.x + v1.y * v2.y;
const cosine_between = (v1, v2) => dot_product(v1, v2) / (length(v1) * length(v2));
const cos_a = cosine_between(BA, BC);
const tan_half_a = Math.sqrt((1 - cos_a) / (1 + cos_a));
const BF_length = radius / tan_half_a;
计算 BF 向量。我们现在知道它的长度(上面的 BF_lengthBF 与向量 BA 在同一条直线上,因此可以通过将 BA 的单位向量进行标量乘法来计算前者(以及由此推导出的点 F 相对于点 B 的坐标):
/// 辅助函数
const unit = v => {
const l = length(v);
return { x: v.x / l, y: v.y / l };
};
const scalar_multiply = (v, n) => ({ x: v.x * n, y: v.y * n });
const BF = scalar_multiply(unit(BA), BF_length);
现在您已经从前一步骤中得到
这就是全部内容——由于您已经在与原始点(A、B和C)具有相同空间坐标的点O,您可以将使用的半径圆心放在O处。
可选项
计算从点F和一些F'(其等效于BC
向量上的点)得出相应的圆弧应该相当容易,但除非有人要求,否则我不会包括它。
术语
这对于大多数使用此答案的人可能是显而易见的,但为了安全起见,请记住,在此答案中,我通常将向量和坐标称为同一种度量方式--向量具有元数,即其组件数量;对于二维坐标系统,元数显然为2。因此,向量对象并没有特别编码其“起点”,只有“终点”--由于只有两个分量,暗示着向量“起点”在坐标系原点。例如,向量BA
确实是点B
和A
之间的向量,但由于程序仅存储向量的两个分量(代码段中的x
和y
),因此就好像将向量移动,使得点B
现在位于坐标系原点。点也由两个分量组成,因此“向量”和“点”是可互换的。您必须非常清楚地理解这一点,否则我提供的某些计算可能会在某些时候看起来很奇怪。如果您将向量视为每个具有两个元素的“一维”数组,则可能更容易。实际上,这就是我最初编写这些内容的方式,但出于说明代码解决方案的目的,我切换到了具有x
和y
属性的对象。
/// <summary>
/// Round polygon corners
/// </summary>
/// <param name="points">Vertices array</param>
/// <param name="radius">Round radius</param>
/// <returns></returns>
static public GraphicsPath RoundCorners(PointF[] points, float radius) {
GraphicsPath retval = new GraphicsPath();
if (points.Length < 3) {
throw new ArgumentException();
}
rects = new RectangleF[points.Length];
PointF pt1, pt2;
//Vectors for polygon sides and normal vectors
Vector v1, v2, n1 = new Vector(), n2 = new Vector();
//Rectangle that bounds arc
SizeF size = new SizeF(2 * radius, 2 * radius);
//Arc center
PointF center = new PointF();
for (int i = 0; i < points.Length; i++) {
pt1 = points[i];//First vertex
pt2 = points[i == points.Length - 1 ? 0 : i + 1];//Second vertex
v1 = new Vector(pt2.X, pt2.Y) - new Vector(pt1.X, pt1.Y);//One vector
pt2 = points[i == 0 ? points.Length - 1 : i - 1];//Third vertex
v2 = new Vector(pt2.X, pt2.Y) - new Vector(pt1.X, pt1.Y);//Second vector
//Angle between vectors
float sweepangle = (float)Vector.AngleBetween(v1, v2);
//Direction for normal vectors
if (sweepangle < 0) {
n1 = new Vector(v1.Y, -v1.X);
n2 = new Vector(-v2.Y, v2.X);
}
else {
n1 = new Vector(-v1.Y, v1.X);
n2 = new Vector(v2.Y, -v2.X);
}
n1.Normalize(); n2.Normalize();
n1 *= radius; n2 *= radius;
/// Points for lines which intersect in the arc center
PointF pt = points[i];
pt1 = new PointF((float)(pt.X + n1.X), (float)(pt.Y + n1.Y));
pt2 = new PointF((float)(pt.X + n2.X), (float)(pt.Y + n2.Y));
double m1 = v1.Y / v1.X, m2 = v2.Y / v2.X;
//Arc center
if (v1.X == 0) {// first line is parallel OY
center.X = pt1.X;
center.Y = (float)(m2 * (pt1.X - pt2.X) + pt2.Y);
}
else if (v1.Y == 0) {// first line is parallel OX
center.X = (float)((pt1.Y - pt2.Y) / m2 + pt2.X);
center.Y = pt1.Y;
}
else if (v2.X == 0) {// second line is parallel OY
center.X = pt2.X;
center.Y = (float)(m1 * (pt2.X - pt1.X) + pt1.Y);
}
else if (v2.Y == 0) {//second line is parallel OX
center.X = (float)((pt2.Y - pt1.Y) / m1 + pt1.X);
center.Y = pt2.Y;
}
else {
center.X = (float)((pt2.Y - pt1.Y + m1 * pt1.X - m2 * pt2.X) / (m1 - m2));
center.Y = (float)(pt1.Y + m1 * (center.X - pt1.X));
}
rects[i] = new RectangleF(center.X - 2, center.Y - 2, 4, 4);
//Tangent points on polygon sides
n1.Negate(); n2.Negate();
pt1 = new PointF((float)(center.X + n1.X), (float)(center.Y + n1.Y));
pt2 = new PointF((float)(center.X + n2.X), (float)(center.Y + n2.Y));
//Rectangle that bounds tangent arc
RectangleF rect = new RectangleF(new PointF(center.X - radius, center.Y - radius), size);
sweepangle = (float)Vector.AngleBetween(n2, n1);
retval.AddArc(rect, (float)Vector.AngleBetween(new Vector(1, 0), n2), sweepangle);
}
retval.CloseAllFigures();
return retval;
}
这里有一种使用几何学的方法:
以上仅适用于在原点相交且Y轴将它们之间的角度平分的线。但是对于所有角落都同样适用,您只需要在应用上述方法之前进行旋转和平移即可。此外,您需要选择一些交点的X值,从中绘制弧线。这些值不应太远或靠近原点。
这与其他答案有点偏离,但也更简单。
我们将处理很多角度,因此需要使用许多arctan2来计算带公差的角度,因此要知道如何使用它。你还可以通过行列式来计算,因为在0附近,这些线条几乎是平行的。
找到想要简化的角度,比如任何大于某个角度阈值的角度。执行以下三个操作:
向
而不是到
。因此,其中amount是可调节的:amount *(p1-p2)+ p2。
- 简化:查找任何角度未超过某个阈值的点,比如说1%,然后继续删除那些点(您还可以检查中点与中心点之间的偏差)。这通常会撤消分割,但我们已经对其进行了平滑处理,并且原始的锐角已经扩散成曲线。
你可以注意到,应用平滑的次数将仅传播那么多线段的角度。因此,如果您将其分段为50个部分。如果您只应用平滑10次,则仅最靠近拐角的10条线可能具有修改后的角度。
所以你只需要选择一些值,你要将多少行分段?你要应用多少平滑?你要应用平滑多少次?当你简化回来时,多大的偏差足以保留角度。
t=R/sin(a/2)
,其中t
是圆心到角点的距离,a
是角的大小。 - xiaofeng.li