将SVG圆弧表示为一系列曲线 [英] Express SVG arc as series of curves

查看:112
本文介绍了将SVG圆弧表示为一系列曲线的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试准确地将SVG路径表示为UIBezierPath,但是可悲的是,UIBezierPath上的addArc不能解释椭圆,只能解释圆(半径只有1个值).

I'm trying to accurately express an SVG Path as a UIBezierPath however sadly the addArc on UIBezierPath does not account for ellipses, only circles (only 1 value for radius).

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

我的想法是将弧线分解为svg曲线,但是我不确定如何计算.

My thinking would be to break the arc into pieces as svg curves, but I'm not sure how to calculate it.

如果我知道要制作的形状,可以说右上角的弧形

If I know the shape I want to make I can turn say, the top right corner arc

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

但是当我解析任何可能的弧线时,我需要知道如何分解任何椭圆形以及计算每个Bezier曲线的控制点.

but as I'll be parsing any possible arc, I need to know how to break up any ellipse and also calculate control points for each of the Bezier curves.

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

I've been looking at various resources that show cubic curves calculated in this way... http://www.spaceroots.org/documents/ellipse/node12.html

但是我不确定如何用代码表达这一点

but I'm not sure how to express this in code

这是我到目前为止所拥有的....

This is what I have so far....

SVG中a路径的值

半径X 半径Y rotationOfArcX isLarge isSweep destinationX destinationY

radiusX radiusY rotationOfArcX isLarge isSweep destinationX destinationY

修改

@Spektre,当我绘制一些简单路径时,您的答案看起来不错,但是路径是根据大+扫掠组合移动的.

@Spektre your answer looks great when I render out some simple paths but the path is moving depending on large + sweep combination.

例如

小扫频/大扫频

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

^^大扫除示例

我的弧的代码版本

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弧转换为直线

  1. see Converting an svg arc to lines

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

It will compute any point on the SVG elliptic arc by parameter so you can create as many control points as you want.

使用插值三次方

看看:

尤其是那里的最后一个链接:

especially the last link from there:

将插值三次控制点直接转换为BEZIER三次控制点.

as it converts the interpolation cubic control points directly to BEZIER cubic control points.

因此将弧线划分为n个点.形成4点立方块并将其转换为BEZIER ...

So divide your arc into n points. Form 4 point cubic patches and convert them to BEZIERs ...

请注意,整个椭圆至少需要4立方,但最好是8立方,因此与原始形状的偏差不会太大.因此,根据圆弧的角度大小,确定0..360 deg

Beware you need at least 4 cubics for whole ellipse but 8 is better so you do not have too big deviation from original shape. So based on the angular size of the arc decide how many cubics you need 1..8 for 0..360 deg

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

Do not forget to handle the edges of the elliptic curve by extrapolating 1st and last control point slightly outside the angle range of the arc so the 1st derivation is not screwed ...

[Edit1]示例...

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

Let us consider this simple 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 this:

使用以下命令呈现预计算的值之后

After rendering the precomputed values using:

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

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

source is below... Will give:

添加一些解释:

(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渲染叠加层:

Now I created simplified C++ example that computes everything and render overlay with GL in my SVG editor engine:

//---------------------------------------------------------------------------
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)注释,可以删除.

where svg2scr converts from SVG units into my GL view coordinates and draw_line render debug output so you can ignore them. The _acc_zero_ang=0.000001*M_PI/180.0 is just accuracy constant. The unimportant stuff is taged with (ignore) comment and can be deleted.

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

  1. Now magenta is the SVG rendered elliptic arc.

起点未按角度旋转(蓝线不居中).

The start end point is unrotated by angle (blue line not going to center).

这将使椭圆轴对齐,因此将其y轴缩放为a/b会将其变成半径为a的圆(红线不会居中).从其中点开始投射一条垂直线(该侧取决于扫掠/弧度).哪个必须沿某处击中圆心.

That makes the ellipse axis aligned so scaling its y axis by a/b will turn it into circle with radius a (red line not going to center). From its mid point is cast a perpendicular line (which side depends on sweep/larc). Which must hit the circle center along the way somewhere.

圆的中心/中点/起点或终点形成一个直角三角形,因此使用毕达哥拉斯,我计算了从中点到中心的距离.转换为vx,vy向量的比例尺"l".

The circle center/midpoint/start or end point form a right angle triangle so using Pythagoras I compute the distance from mid point to center. Converted to scale 'l' of the vx,vy vector.

一旦获得未旋转的中心圆sx,sy,您就可以使用atan2

Once you got the center unrotated circle sx,sy you can compute edge angles a0,a1 of the arc using atan2

现在通过按b/a(蓝色)缩放y轴来缩放回椭圆.

Now scale back to ellipse by scaling y axis by b/a (blue)

现在将(sx,sy)中心向后旋转ang,得到(cx,cy)就是您所需要的(红色)

Now rotate the (sx,sy) center back by ang getting (cx,cy) is all you need (red)

现在,我们终于可以在椭圆上获得任何点,因此我们可以转换为BEZIER立方.这里是原始椭圆(品红色)和新BEZIER(红色)路径的叠加.

Now we can finally obtain any point on the ellipse so we can convert to BEZIER cubics. Here overlay of original ellipse (magenta) and new BEZIER (red) paths.

请注意,此处的缩放比例与它们不完全匹配:

Beware they do not match precisely here zoom:

  1. 根据|a1-a0|

看起来每360度16个BEZIER立方就足够了.精度越高...在这种情况下,生成的n=3立方

looks like 16 BEZIER cubics per 360 deg is sort of enough. The more the higher precision... In this case resulting n=3 cubics

获得n+3内插三次控制点

每个立方需要4个点,但是它在第二个和第三个之间绘制曲线,因此将剩下2个点.这意味着我们需要获得稍微超出a0,a1范围的值,以使形状不会失真.控制点只是椭圆上的点(十字)...

each cubic needs 4 points but it renders curve between second and third one so there will be 2 points left over. That means we need to obtaine them slightly outside a0,a1 range so the shape will not be distorted. The control points are simply the points on the ellipse (crosses)...

为每个插值三次方创建BEZIER对应项

for each interpolation cubic create BEZIER counterpart

只需使用上面链接中的公式即可在两个立方体之间转换.

simply use the formula from link above to transfom between the two cubics.

保存新的SVG.

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

I did just use txt string variable that hold the new path and added it to test svg manualy.

此处是合并的路径:

<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>

这篇关于将SVG圆弧表示为一系列曲线的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆