将 SVG 圆弧表示为一系列曲线

Express SVG arc as series of curves

我试图将 SVG 路径准确地表示为 UIBezierPath,但遗憾的是 UIBezierPath 上的 addArc 不考虑椭圆,只考虑圆(半径只有 1 个值).

bezierPath.addArc(withCenter:CGPoint radius:CGFloat startAngle:CGFloat endAngle:CGFloat clockwise:Bool)

我的想法是将圆弧分解成 svg 曲线,但我不确定如何计算它。

如果我知道我想做的形状我可以转过来说,右上角的圆弧

a150,150 0 1,0 150,-150成曲线c82.84,0,150,44.77,150,100

但是因为我要解析任何可能的弧线,所以我需要知道如何分解任何椭圆并计算每条贝塞尔曲线的控制点。

我一直在查看各种显示以这种方式计算的三次曲线的资源... http://www.spaceroots.org/documents/ellipse/node12.html

但我不确定如何在代码中表达这一点

这是我目前所拥有的....

SVG 中 a 路径的值

半径X 半径Y 旋转的ArcX 是大 是扫描 目的地X 目的地Y

编辑

@Spektre 当我渲染出一些简单的路径时,你的答案看起来不错,但路径会根据大 + 扫描组合移动。

例如

小扫描/大不扫描

M 180.0 80.0 a50,50 0 0,1 50,50 z

M 180.0 80.0 a50,50 0 1,0 50,50 z

X 已翻译 +100

M 180.0 80.0
M 280.0 80.0
C 280.0 73.62 278.63 66.76 276.19 60.87
C 273.75 54.97 269.87 49.15 265.36 44.64
C 260.85 40.13 255.03 36.25 249.13 33.81
C 243.24 31.37 236.38 30.0 230.0 30.0
z

^^小扫示例

小无扫描/大扫描

M 180.0 80.0 a50,50 0 0,0 50,50 z

M 180.0 80.0 a50,50 0 1,1 50,50 z

Y 已翻译 +100

M 180.0 80.0
M 180.0 180.0
C 186.38 180.0 193.24 178.63 199.13 176.19
C 205.03 173.75 210.85 169.87 215.36 165.36
C 219.87 160.85 223.75 155.03 226.19 149.13
C 228.63 143.24 230.0 136.38 230.0 130.0
C 230.0 123.62 228.63 116.76 226.19 110.87
C 223.75 104.97 219.87 99.15 215.36 94.64
C 210.85 90.13 205.03 86.25 199.13 83.81
C 193.24 81.37 186.38 80.0 180.0 80.0
C 173.62 80.0 166.76 81.37 160.87 83.81
C 154.97 86.25 149.15 90.13 144.64 94.64
C 140.13 99.15 136.25 104.97 133.81 110.87
C 131.37 116.76 130.0 123.62 130.0 130.0
z

^^大扫示例

我的 arc 代码版本

M 10 70 a 133.591805 50 12.97728 0 0 70 -50 z

M 10.0 70.0
M 65.33 62.67
C 53.75 67.15 35.85 69.91 17.44 70.06
C -0.97 70.2 -24.36 67.78 -45.14 63.57
C -65.92 59.36 -89.13 52.34 -107.24 44.79
z

我的代码版本


private func arcAsCurves(x0: CGFloat, y0: CGFloat, a: CGFloat, b: CGFloat, angle: CGFloat, large: Bool, sweep: Bool, x1: CGFloat, y1: CGFloat) -> String {

        //return "L\(x1) \(y1)"

        var localSweep = sweep
        if large { localSweep = !localSweep }

        let pi = CGFloat.pi
        let pi2 = pi*2
        let ang = pi-(angle*pi/180.0) // [deg] -> [rad] and offset to match my coordinate system

        let e = a/b
        var c = cos(+ang)
        var s = ang == pi ? 0.0 : sin(+ang)

        let ax = x0*c-y0*s // (ax,ay) = unrotated (x0,y0)
        var ay = x0*s+y0*c
        let bx = x1*c-y1*s // (bx,by) = unrotated (x1,y1)
        var by = x1*s+y1*c

        ay *= e // transform ellipse to circle by scaling y axis
        by *= e

        // rotated centre by angle

        let axd = ax+bx
        let ayd = ay+by

        var sx = 0.5 * axd // mid point between A,B
        var sy = 0.5 * ayd

        var vx = ay-by // perpendicular direction vector to AB of size |AB|
        var vy = bx-ax

        var l = (a*a / (vx*vx + vy*vy)) - 0.25 // compute distance of center to (sx,sy) from pythagoras
        //l=divide(a*a,(vx*vx)+(vy*vy))-0.25

        if l < 0 { // handle if start/end points out of range (not on ellipse) center is in mid of the line
            l = 0
        }

        l = sqrt(l)
        vx *= l  // rescale v to distance from id point to center
        vy *= l

        if localSweep { // pick the center side
            sx += vx
            sy += vy
        } else {
            sx -= vx
            sy -= vy
        }

//        sx += localSweep ? vx : -vx
//        sy += localSweep ? vy : -vy

        var a0 = atan2(ax-sx, ay-sy) // compute unrotated angle range
        var a1 = atan2(bx-sx, by-sy)

        // a0 = atanxy(ax-sx,ay-sy);
        // a1 = atanxy(bx-sx,by-sy);

        ay /= e
        by /= e
        sy /= e // scale center back to ellipse

        // pick angle range
        var da = a1-a0
        let zeroAng = 0.000001 * pi/180.0

        if abs(abs(da)-pi) <= zeroAng { // half arc is without larc and sweep is not working instead change a0,a1

            var db = (0.5 * (a0+a1)) - atan2(bx-ax,by-ay)

            while (db < -pi) { db += pi2 } // db<0 CCW ... sweep=1
            while (db > pi) { db -= pi2 } // db>0  CW ... sweep=0

            if (db < 0.0 && !sweep) || (db > 0.0 && sweep) {
                if da >= 0.0 { a1 -= pi2 }
                if da < 0.0 { a0 -= pi2 }
            }
        }
        else if large {
            if da < pi && da >= 0.0 { a1 -= pi2 }
            if da > -pi && da < 0.0 { a0 -= pi2 }
        }
        else {
            if da > pi { a1 -= pi2 }
            if da < -pi { a0 -= pi2 }
        }

        da = a1-a0

        c = cos(-ang)
        s = sin(-ang)

//        var cx = sx*c-sy*s // don't need this
//        var cy = sx*s+sy*c

        var n: Int = 0
        let maxCount: Int = 16

        var dt: CGFloat = 0.0

        var px = [CGFloat]()
        var py = [CGFloat]()

        n = Int(abs((CGFloat(maxCount) * da)/pi2))

        if n < 1 { n = 1 }
        else if n > maxCount { n = maxCount }

        dt = da / CGFloat(n)

        // get n+3 points on ellipse (with edges uniformly outside a0,a1)

        let t = a0 - dt

        for i in 0..<n+3 {

            // point on axis aligned ellipse
            let tt = t + (dt*CGFloat(i))
            let xx = sx+a*cos(tt)
            let yy = sy+b*sin(tt)

            // rotate by ang
            let c: CGFloat = cos(-ang)
            let s: CGFloat = sin(-ang)

            px.append(xx*c-yy*s)
            py.append(xx*s+yy*c)
        }

        let m: CGFloat = 1/6

        var string = ""

        for i in 0..<n
        {
            // convert to interpolation cubic control points to BEZIER
            let x0 = px[i+1];                     let y0 = py[i+1];
            let x1 = px[i+1]-(px[i+0]-px[i+2])*m; let y1 = py[i+1]-(py[i+0]-py[i+2])*m;
            let x2 = px[i+2]+(px[i+1]-px[i+3])*m; let y2 = py[i+2]+(py[i+1]-py[i+3])*m;
            let x3 = px[i+2];                     let y3 = py[i+2];

            if i == 0 {
                let mString = String(format: "M%.2f %.2f", x0, y0)
                string.append(mString)
            }

            let cString = String(format: "C%.2f %.2f %.2f %.2f %.2f %.2f", x1, y1, x2, y2, x3, y3)
            string.append(cString)
        }

        return string
    }
  1. 它将通过参数计算 SVG 椭圆弧上的任何点,因此您可以根据需要创建任意数量的控制点。

  2. 使用插值三次

    看看:

    尤其是最后一个 link 来自那里:

    • Interpolation cubic vs. Bezier cubic

    因为它将插值立方控制点直接转换为 BEZIER 立方控制点。

    所以将你的弧分成 n 个点。形成 4 点立方块并将它们转换为 BEZIERs ...

    请注意,整个椭圆至少需要 4 个立方体,但 8 个更好,这样您就不会与原始形状有太大偏差。因此,根据弧的 angular 大小决定 0..360 deg

    需要多少立方体 1..8

    不要忘记通过将第一个和最后一个控制点稍微外推到圆弧的角度范围之外来处理椭圆曲线的边缘,这样一阶推导就不会搞砸了...

[Edit1] 示例 ...

让我们考虑一下这个简单的 SVG:

<svg width="512" height="512" viewBox="3.621934 13.621934 90.255485 62.818094" fill="none" stroke="none" stroke-width="1px" transform="matrix(1,0,0,1,0,0" >
 <g>
  <path id=" " stroke="magenta" d="M 10 70 a 133.591805 50 12.97728 0 0 70 -50 "/>
 </g>
</svg>

So (no)/unit matrix, single arc path looking like:

渲染预计算值后使用:

_test_ellarc(10,70,133.591806,50.0,12.97728,0,0,80,20);

来源在下面...将给出:

添加一些解释:

(x0,y0) = (10,70)    // last point before 'a'
      a = 133.591805
      b = 50
    ang = 12.97728 deg
  sweep = 0 
   larc = 0 
(x1,y1) = (80,20)    // lower case 'a' means relative coordinates to x0,y0

现在我创建了简化的 C++ 示例,它计算所有内容并在我的 SVG 编辑器引擎中使用 GL 渲染叠加层:

//---------------------------------------------------------------------------
void svg2scr(double *p,double x,double y) // SVG(x,y) -> OpenGL(p[3])
    {
    p[0]=x;
    p[1]=y;
    p[2]=0.0;
    win_SVGEditor->edit.scl2g_svg2ogl.l2g(p,p);
    }
void draw_line(double x0,double y0,double x1,double y1,double r,double g,double b)
    {
    double p0[3],p1[3];
    glBegin(GL_LINES);
    glColor3f(r,g,b);
    svg2scr(p0,x0,y0); glVertex2dv(p0);
    svg2scr(p1,x1,y1); glVertex2dv(p1);
    glEnd();
    }
//---------------------------------------------------------------------------
void _test_ellarc(double x0,double y0,double a,double b,double ang,bool larc,bool sweep,double x1,double y1)
    {
    // ang [deg]
    // x0,y0,x1,y1 are absolute !!!

    // (ignore) init for rendering
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    // -----------------------------------------
    // [SVG elliptic arc to parametric ellipse]
    // -----------------------------------------

//  draw_line(x0,y0,x1,y1,1.0,0.0,0.0); // raw start-end point line (red)

    // precomputed constants
    double sx,sy,a0,a1,da;      // sx,sy rotated center by ang
    double cx,cy;               // real center
    // helper variables
    double  ax,ay,bx,by;
    double  vx,vy,l,db;
    int     _sweep;
    double  c,s,e;

    ang=M_PI-(ang*M_PI/180.0);  // [deg] -> [rad] and offset to match my coordinate system
    _sweep=sweep;
    if (larc) _sweep=!_sweep;

    e=divide(a,b);
    c=cos(+ang);
    s=sin(+ang);
    ax=x0*c-y0*s;           // (ax,ay) = unrotated (x0,y0)
    ay=x0*s+y0*c;
    bx=x1*c-y1*s;           // (bx,by) = unrotated (x1,y1)
    by=x1*s+y1*c;

    ay*=e;                  // transform ellipse to circle by scaling y axis
    by*=e;

    sx=0.5*(ax+bx);         // mid point between A,B
    sy=0.5*(ay+by);
    vx=(ay-by);             // perpendicular direction vector to AB of size |AB|
    vy=(bx-ax);

/*  pythagoras:
    |v|=|b-a|
    (|v|/2)^2 + l^2 = r^2
    l^2 = r^2 - (|v|/2)^2
    l^2 = r^2 - |v|^2 * 0.25
    l^2/|v|^2 = r^2/|v|^2 - 0.25
*/
    l=divide(a*a,(vx*vx)+(vy*vy))-0.25; // compute distance of center to (sx,sy) from pythagoras
    if (l<0) l=0;           // handle if start/end points out of range (not on ellipse) center is in mid of the line
    l=sqrt(l);
    vx*=l;                  // rescale v to distance from id point to center
    vy*=l;

    // (ignore) perpendicular line going through both centers (dark GREEN)
//  draw_line(sx-vx,sy-vy,sx+vx,sy+vy,0.0,0.3,0.0);

    if (_sweep)             // pick the center side
        {
        sx+=vx;
        sy+=vy;
        }
    else{
        sx-=vx;
        sy-=vy;
        }

    a0=atanxy(ax-sx,ay-sy); // compute unrotated angle range
    a1=atanxy(bx-sx,by-sy);
/*
    // (ignore) unrotated scaled to circle center and start-end points (GREEN)
    draw_line(ax,ay,bx,by,0.0,0.7,0.0);
    draw_line(ax,ay,sx,sy,0.0,0.7,0.0);
    draw_line(bx,by,sx,sy,0.0,0.7,0.0);
    // (ignore) unrotated scaled to circle circle arc a0..a1 (GREEN)
    glBegin(GL_LINE_STRIP);
    glColor3f(0.0,0.7,0.0);
    for (double aaa=a0,daa=(a1-a0)*0.05,p[3],i=0;i<=20;aaa+=daa,i++)
     { svg2scr(p,sx+a*cos(aaa),sy+a*sin(aaa)); glVertex2dv(p); }
    glEnd();
*/
    ay=divide(ay,e);
    by=divide(by,e);
    sy=divide(sy,e);        // scale center back to ellipse
/*
    // (ignore) unrotated ellipse center and start-end points (BLUE)
    draw_line(ax,ay,bx,by,0.0,0.0,0.7);
    draw_line(ax,ay,sx,sy,0.0,0.0,0.7);
    draw_line(bx,by,sx,sy,0.0,0.0,0.7);
    // (ignore) unrotated ellipse arc a0..a1 (BLUE)
    glBegin(GL_LINE_STRIP);
    glColor3f(0.0,0.0,0.7);
    for (double aaa=a0,daa=(a1-a0)*0.05,p[3],i=0;i<=20;aaa+=daa,i++)
     { svg2scr(p,sx+a*cos(aaa),sy+b*sin(aaa)); glVertex2dv(p); }
    glEnd();
*/
    // pick angle range
    da=a1-a0;
    if (fabs(fabs(da)-pi)<=_acc_zero_ang)       // half arc is without larc and sweep is not working instead change a0,a1
        {
        db=(0.5*(a0+a1))-atanxy(bx-ax,by-ay);
        while (db<-pi) db+=pi2;     // db<0 CCW ... sweep=1
        while (db>+pi) db-=pi2;     // db>0  CW ... sweep=0
        _sweep=0;
        if ((db<0.0)&&(!sweep)) _sweep=1;
        if ((db>0.0)&&( sweep)) _sweep=1;
        if (_sweep)
            {
//          a=0; b=0;
            if (da>=0.0) a1-=pi2;
            if (da< 0.0) a0-=pi2;
            }
        }
    else if (larc)              // big arc
        {
        if ((da< pi)&&(da>=0.0)) a1-=pi2;
        if ((da>-pi)&&(da< 0.0)) a0-=pi2;
        }
    else{                       // small arc
        if (da>+pi) a1-=pi2;
        if (da<-pi) a0-=pi2;
        }
    da=a1-a0;

    // rotated center
    c=cos(-ang);
    s=sin(-ang);
    cx=sx*c-sy*s;
    cy=sx*s+sy*c;
/*
    // (ignore) rotated center and start-end point (RED)
    draw_line(x0,y0,x1,y1,1.0,0.0,0.0);
    draw_line(x0,y0,cx,cy,1.0,0.0,0.0);
    draw_line(x1,y1,cx,cy,1.0,0.0,0.0);
*/
    // -----------------------------------------
    // [parametric ellipse to BEZIER cubics]
    // -----------------------------------------

    int i,n;
    const int N=16;         // cubics per whole ellipse
    double t,dt;
    double px[N+3],py[N+3]; // all interpolation cubics control points
    double w=2.5; // rendered cross size
    // arclength 0..2*PI -> cubics count 1..8
    n=fabs(double(N)*da)/(2.0*M_PI);
    if (n<1) n=1;
    if (n>N) n=N;
    dt=da/double(n);
    // get n+3 points on ellipse (with edges uniformly outside a0,a1)
    for (t=a0-dt,i=0;i<n+3;i++,t+=dt)
        {
        double c,s,xx,yy;
        // point on axis aligned ellipse
        xx=sx+a*cos(t);
        yy=sy+b*sin(t);
        // rotate by ang
        c=cos(-ang);
        s=sin(-ang);
        px[i]=xx*c-yy*s;
        py[i]=xx*s+yy*c;
        // render
        draw_line(px[i]-w,py[i]+w,px[i]+w,py[i]-w,0.5,0.2,0.7);
        draw_line(px[i]-w,py[i]-w,px[i]+w,py[i]+w,0.5,0.2,0.7);
        }
    // process cubics
    AnsiString txt="";
    for (i=0;i<n;i++)
        {
        const double m=1.0/6.0;
        double x0,y0,x1,y1,x2,y2,x3,y3;
        // convert to interpolation cubic control points to BEZIER
        x0 = px[i+1];                     y0 = py[i+1];
        x1 = px[i+1]-(px[i+0]-px[i+2])*m; y1 = py[i+1]-(py[i+0]-py[i+2])*m;
        x2 = px[i+2]+(px[i+1]-px[i+3])*m; y2 = py[i+2]+(py[i+1]-py[i+3])*m;
        x3 = px[i+2];                     y3 = py[i+2];
        // render
        if (!i) txt+=AnsiString().sprintf("M%.6lf %.6lf",x0,y0);
                txt+=AnsiString().sprintf(" C%.6lf %.6lf %.6lf %.6lf %.6lf %.6lf",x1,y1,x2,y2,x3,y3);
        }
    // here save the txt into your SVG path

    // (ignore) exit from rendering
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
    }
//---------------------------------------------------------------------------

其中 svg2scr 将 SVG 单位转换为我的 GL 视图坐标,并且 draw_line 呈现调试输出以便您可以忽略它们。 _acc_zero_ang=0.000001*M_PI/180.0 只是精度常数。不重要的东西用 (ignore) 注释标记,可以删除。

  1. 现在洋红色是SVG渲染的椭圆弧。

  2. 起点终点未旋转角度(蓝线未居中)。

    这使得椭圆轴对齐,因此将其 y 轴缩放 a/b 会将其变成半径为 a 的圆(红线不会居中)。从它的中点开始投射一条垂直线(哪一边取决于 sweep/larc)。一定会在途中某处击中圆心

  3. 圆 center/midpoint/start 或端点形成直角三角形,因此我使用毕达哥拉斯计算中点到中心的距离。转换为 vx,vy 向量的比例 'l'。

  4. 一旦你得到中心未旋转的圆 sx,sy 你可以使用 atan2

    [=150= 计算圆弧的边角 a0,a1 ]
  5. 现在通过将 y 轴缩放 b/a(蓝色)

  6. 来缩小回椭圆
  7. 现在将 (sx,sy) 中心向后旋转 ang 得到 (cx,cy) 就是你所需要的(红色)

现在我们终于可以得到椭圆上的任意一点,这样我们就可以转换为BEZIER立方体了。这里覆盖了原始椭圆(洋红色)和新的 BEZIER(红色)路径。

注意它们在此处不完全匹配缩放:

  1. 根据|a1-a0|

    决定需要多少(n)立方

    看起来每 360 度 16 个 BEZIER 立方体就足够了。越多精度越高...本例结果为n=3cubics

  2. 获得n+3个插值三次控制点

    每个立方体需要 4 个点,但它呈现第二个和第三个之间的曲线,因此将剩下 2 个点。这意味着我们需要将它们稍微超出 a0,a1 范围,这样形状就不会变形。控制点只是椭圆(十字)上的点...

  3. 为每个插值立方体创建 BEZIER 对应物

    只需使用上面link的公式即可在两个三次方之间进行转换。

  4. 保存新的 SVG。

    我只是使用 txt 保存新路径的字符串变量并将其添加到手动测试 svg。

这里是合并路径:

<svg width="512" height="512" viewBox="3.621934 13.621934 90.255485 62.818094" fill="none" stroke="none" stroke-width="1px" transform="matrix(1,0,0,1,0,0" >
 <g stroke="blue">
  <path id=" " stroke="magenta" d="M 10 70 a 133.591805 50 12.97728 0 0 70 -50 "/>
  <path id=" " stroke="red"     d="M10.000000 70.000000 C24.500960 70.325512 38.696601 69.272793 49.846109 67.045096 C60.995616 64.817400 70.632828 61.108261 76.897046 56.633820 C83.161264 52.159379 86.914255 46.304086 87.431414 40.198450 C87.948573 34.092813 85.301045 26.896880 80.000000 20.000000 "/>
 </g>
</svg>