用于在多边形中创建圆角的算法

时间:2014-07-16 03:41:26

标签: c# objective-c algorithm polygon rounded-corners

我正在寻找一种允许我从多边形创建圆角的算法。 在Input中,我得到一个表示多边形(红线)和输出的点数组,一个点数组,表示带圆角的多边形(黑线)。

我还想有办法控制每个角落的半径。 我已经尝试过使用Bezier和Subdivision,但这不是我想要的。 Bezier和Subdivision正在平滑所有多边形。我想要的只是让角落四舍五入。

有人知道这样做有什么好的算法吗? 我正在使用C#,但代码必须独立于任何.NET库。

Example

6 个答案:

答案 0 :(得分:61)

使用Paint的几何几何:


你有一个角落:
Corner

1.你知道角点的坐标,让它为P 1 ,P 2 和P:
Points of corner

2.现在你可以从矢量之间的点和角度得到矢量:
Vectors and angle

angle = atan(PY - P1Y, PX - P1X) - atan(PY - P2Y, PX - P2X)


3.获得角点与圆的交点之间的段长度 Segment

segment = PC1 = PC2 = radius / |tan(angle / 2)|


4.在这里你需要检查段的长度和PP 1 和PP 2 的最小长度:
Minimal length
PP的长度 1

PP1 = sqrt((PX - P1X)2 + (PY - P1Y)2)

PP的长度 2

PP2 = sqrt((PX - P2X)2 + (PY - P2Y)2)

如果分段> PP 1 或段> PP 2 然后你需要减小半径:

min = Min(PP1, PP2) (for polygon is better to divide this value by 2)
segment > min ?
    segment = min
    radius = segment * |tan(angle / 2)|


5.获取PO的长度:

PO = sqrt(radius2 + segment2)


6.通过矢量坐标之间的比例,长度获得C 1 X 和C 1 Y 矢量和段的长度:
Coordinates of PC1

比例:

(PX - C1X) / (PX - P1X) = PC1 / PP1

所以:

C1X = PX - (PX - P1X) * PC1 / PP1

对于C 1 Y

C1Y = PY - (PY - P1Y) * PC1 / PP1


7.以相同的方式获取C 2 X 和C 2 Y

C2X = PX - (PX - P2X) * PC2 / PP2
C2Y = PY - (PY - P2Y) * PC2 / PP2


8.现在您可以使用添加向量PC 1 和PC 2 以相同的方式按比例找到圆心:
Addition of vectors

(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 = sqrt(dx2 + dy2)

OX = PX - dx * PO / PC
OY = PY - dy * PO / PC


9.在这里你可以画一个圆弧。为此,您需要获得弧的起始角度和结束角度:
Arc
找到它here

startAngle = atan((C1Y - OY) / (C1X - OX))
endAngle = atan((C2Y - OY) / (C2X - OX))


10.最后你需要获得一个扫掠角度并对其进行一些检查:
Sweep angle

sweepAngle = endAngle - startAngle

如果sweepAngle< 0然后交换startAngle和endAngle,并反转sweepAngle:

sweepAngle < 0 ?    
    sweepAngle = - sweepAngle
    startAngle = endAngle

检查sweepAngle&gt; 180度:

sweepAngle > 180 ?    
    sweepAngle = 180 - sweepAngle


11.现在你可以画一个圆角:
The result

c#的几何几何:

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);
}

答案 1 :(得分:25)

您正在寻找与给定半径的两个连接线段相切的圆弧,由一些连续的点阵列给出。用于查找此弧的算法如下:

  1. 对于每个片段,构建一个法向量。

    1. 如果您在2d工作,则可以减去两个端点以获得切线矢量(X,Y)。在这种情况下,法向量将是正或负(-Y,X)。 Normalize长度为1的法线向量。最后,选择具有正点积的方向和下一段的切线向量。 (请参阅下面的更新)。

    2. 如果你在3d而不是2d工作,为了得到正常的,cross你要圆的顶点上的两个线段的切向量,以得到线的平面的垂直向量。如果垂线的长度为零,则这些段是平行的,不需要圆形。否则,将其标准化,然后与切线垂直交叉以获得正常值。)

  2. 使用法线向量,将每个线段偏向所需半径的多边形内部。要偏移一个段,使用刚才计算的法向量N来偏移其端点,如下所示:P'= P + r * N(线性组合)。

  3. Intersect the two offset lines找到中心。 (这是因为圆的半径矢量始终垂直于其切线。)

  4. 要查找圆与每个线段相交的点,请将圆心向后偏移到每个原始线段。这些将是您的弧的终点。

  5. 确保弧端点在每个线段内,否则您将创建一个自相交的多边形。

  6. 通过两个端点创建一个弧,中心和中心。你确定的半径。

  7. 我手边没有任何合适的绘图软件,但这个图表显示了这个想法:

    enter image description here

    此时,您需要引入类来表示由线段和弧段组成的图形,或者将弧多边形化为适当的精度,并将所有线段添加到多边形。

    更新:我更新了图像,标记了点P1,P2和P3,以及法线向量Norm12和Norm23。标准化法线仅在翻转方向上是唯一的,您应该按如下方式选择翻转:

    • Norm12的dot product(P3 - P2)必须为正。如果是负数,则将Norm12乘以-1.0。如果它为零,则点是共线的,不需要创建圆角。这是因为你想要向P3偏移。

    • Norm23与(P1 - P2)的点积也必须为正,因为您偏向P1。

答案 2 :(得分:6)

nempoBu4 answer的目标C适应:

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 :(得分:1)

这是我在c#上实现dbc的想法:

/// <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;
}

答案 4 :(得分:0)

这是一种使用某种几何的方法: -

  
      
  1. 两条线与圆形内切相切
  2.   
  3. 切线的法线在圆心处相遇。
  4.   
  5. 让线之间的角度为X
  6.   
  7. 圆心对中的角度为K = 360-90 * 2-X = 180-X
  8.   
  9. 让我们将两点切线确定为(x1,y)和(x2,y)
  10.   
  11. 连接点的和弦长度为l =(x2-x1)
  12.   
  13. 在圆圈内,弦长和两个法线长度r(半径)形成等腰三角形
  14.   
  15. 垂直将垂直分成相等的一半的直角三角形。
  16.   
  17. 角度之一是K / 2,侧面是l / 2
  18.   
  19. 使用直角三角形sin(K / 2)=(l / 2)/ r
  20. 的属性   
  21. r =(l / 2)/ sin(K / 2)
  22.   
  23. 但K = 180-X所以r =(1/2)/ sin(90-X / 2)=(1/2)/ cos(X / 2)
  24.   
  25. 因此r =(x2-x1)/(2 * cos(X / 2))
  26.   
  27. 现在只需使用半径r
  28. 从(x1,y)到(x2,y)绘制一个弧   

注意: -

以上仅针对在原点相交的线进行说明,Y轴将它们之间的角度分成两半。但它同样适用于所有角落,只需要在应用上述之前应用旋转和平移。此外,您需要从要绘制圆弧的位置选择一些x值的交点。值不应太远或接近原点

答案 5 :(得分:0)

我可以提供一种简单且非常可计算(人类和算法均可)的方法,该方法可以说是最佳地使用很少的计算——值得注意的是“仅”3 个平方根且没有反三角函数。

由于这是 Stack Overflow 并且我已经使用 JavaScript 和 SVG 通过实际计算验证了这一点,因此我将使用 ECMAScript (JavaScript) 编程语言来帮助解释解决方案。

假设您想要“圆”的某个角由已知点 ABC 组成,其中 B 是“角落”。我指的是向量BABC等——这样的向量的分量,例如BA,每个都定义为点的对应分量/vector A 减去点/矢量 B 的相应分量,按照惯例。

Points A, B and C with lines from A to B and from B to C; a circle that includes the arc for the rounded corner, at point O as its center, line perpendicular to the vector AB going from O to meet some point F between A and B

解决方案描述如下:

  1. 计算BF向量的长度。

    长度等于圆的半径 (FO)(显然是您自己选择并因此知道的)除以向量 BFBO。这显然是因为由点 BOF 构成的三角形是一个“直角”三角形(向量之间的夹角 BF FO 是 90 度)。

    向量BFBO之间的夹角是向量BABC之间夹角的一半。这可能听起来也可能不明显,请放心,这是可以证明的,但我省略了证明,因为它与我的答案相切(没有双关语)。

    角度之间的关系很有用,因为恰好有一个相当简单的方程来表达角度的正切和角度的两倍余弦之间的关系:Math.tan(a/2) == Math.sqrt((1 - Math.cos(a)) / (1 + Math.cos(a))

    恰好向量 BABC 之间夹角的余弦 (Math.cos(a)) 是两个向量的点积除以长度的乘积(见 Wikipedia)。

    因此,在计算了角度的余弦之后,您可以计算半角的正切,以及随后的 BF 的长度:

    (我将向量(BABC 等)编程为具有 xy 属性的对象,用于它们各自在屏幕空间中的坐标(X 向右增加) , Y 向下);radius 是想要的圆角半径,BF_lengthBF 的长度(显然))

    /// Helper functions
    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;
    
  2. 计算 BF 向量。我们现在知道它的长度(上面的BF_length)并且由于 BF 位于向量 BA 所在的同一条线上,这是一种计算向量的方法(在其他话点 F 相对于点 B) 的坐标是将 BF 的长度与单位向量等效的标量乘法BA

    /// Helper functions
    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);
    
  3. 现在您有了上一步中 F 的坐标,您可以计算 FO 向量或 O 坐标。这是通过旋转某个长度为 radius 的向量来完成的它从 F 开始。

    现在,旋转是顺时针还是逆时针取决于矢量BA和BC之间夹角的符号,更具体地说,如果BA夹角的差值em> 和 BC 为正则逆时针旋转,否则顺时针旋转。

    如果可以避免的话,我们不想计算角度——毕竟这是我们想要的差异的标志。长话短说,角的符号 (sign) 可以用表达式 Math.sign(BA.x * BC.y - BA.y * BC.x) 计算。

    这里是 O (O) 坐标的计算,F 是井的坐标,F

    /// Helper functions
    const add = (v1, v2) => ({ x: v1.x + v2.x, y: v1.y + v2.y });
    const rotate_by_90_degrees = (v, sign) => ({ x: -v.y * direction, y: v.x * sign });
    
    const sign = Math.sign(BA.x * BC.y - BA.y * BC.x);
    const O = add(F, rotate_by_90_degrees(scalar_multiply(unit(BA), radius), sign));
    
    

这就是全部——因为你已经获得了点 O 的坐标与原始点 (A, B em> 和 C),你可以用 O 为中心放置一个使用半径的圆。

对于大多数使用这个答案的人来说这可能是显而易见的,但为了安全起见:请记住,在这个答案中,我通常将向量和坐标称为同一种度量——向量具有arity,即它所具有的分量数量;对于二维坐标系统,元数显然是 2。因此,向量对象不会专门对其“开始”进行编码,只对“结束”进行编码——因为只有两个分量,这意味着向量“开始”在坐标系原点。例如,向量 BA 确实是点 BA 之间的向量,但由于程序仅存储向量的两个分量(x 和 {{1} } 在代码片段中),就好像向量被移动,使得点 y 现在位于坐标系的原点。一个点也由两个部分组成,所以“向量”和“点”是可以互换的。你必须非常清楚地理解这一点,否则我提供的一些计算有时可能看起来很奇怪。如果您仅将此答案中的向量视为每个具有两个元素的“一维”数组,则可能会更容易。事实上,这就是我最初编写这些代码的方式,但为了用代码说明解决方案,我切换到具有 Bx 属性的对象。

在相关说明中,从点 F 和一些 F' 计算相应的圆弧可能是有益的,它等效于 y 向量.考虑到所有因素,这应该相当容易,但除非有人表达了愿望,否则我不包括在内。