Выразить SVG дугу как серию кривых - PullRequest
0 голосов
/ 11 декабря 2019

Я пытаюсь точно выразить путь SVG как UIBezierPath, но, к сожалению, addArc на UIBezierPath не учитывает эллипсы, только круги (только 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

но я не уверен, как выразить это в коде

Это то, что у меня есть до сих пор ....

Значения дляa путь в SVG

radiusX radiusY вращениеOfArcX isLarge isSweep destinationX destinationY

Edit

@ 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

^^ пример большой развертки

Мои коды версии вашей дуги

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 Ответ

0 голосов
/ 12 декабря 2019
  1. см. Преобразование SVG-дуги в линии

    Она вычислит любую точку эллиптической дуги SVG по параметру, чтобы вы могли создатьстолько контрольных точек, сколько вы хотите.

  2. используйте интерполяционные кубики

    взгляните на:

    , особенно последняя ссылка оттуда:

    , поскольку он преобразует кубические контрольные точки интерполяции непосредственно в кубические контрольные точки BEZIER.

    Поэтому разделите свою дугу на n точек. Формируйте 4-точечные кубические патчи и конвертируйте их в БЕЗЬЕ ...

    Остерегайтесь, вам нужно как минимум 4 кубических кубика на весь эллипс, но лучше 8, чтобы у вас не было слишком большого отклонения от исходной формы. Поэтому, исходя из углового размера дуги, определите, сколько кубических единиц вам нужно 1..8 для 0..360 deg

    Не забудьте обработать края эллиптической кривой, экстраполируя 1-ю и последнюю контрольную точку немного за пределы угла. диапазон дуги, поэтому 1-й дифференциал не прикручен ...

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

Итак (нет) / единичная матрица, путь одиночной дуги выглядит следующим образом:

preview

После рендеринга предварительно вычисленных значений с использованием:

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

источник находится ниже ... Даст:

computation

Сдобавлены некоторые пояснения:

labels

(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 ++, который вычисляет все и выполняет наложение с помощью GL в моем движке редактора SVG:

//---------------------------------------------------------------------------
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 (красная линия несобирается в центр). От его средней точки проходит перпендикулярная линия (сторона которой зависит от развертки / жаворонка). Который должен попасть в центр круга по пути куда-нибудь.

  3. Центр / средняя точка / начальная или конечная точка круга образуют прямоугольный треугольник, поэтому с помощью Пифагора я вычисляю расстояние от средней точки до центра,Преобразован в масштаб 'l' вектора vx,vy.

  4. После того, как вы получили центральную неповращенную окружность sx,sy, вы можете вычислить углы ребер a0,a1 дуги, используя atan2

  5. Теперь уменьшите масштаб до эллипса, масштабируя ось y на b/a (синий)

  6. Теперь поверните (sx,sy) центр назад на ang получение (cx,cy) - это все, что вам нужно (красный)

Теперь мы наконец можем получить любую точку на эллипсе, чтобы мы могли преобразовать ее в кубы БЕЗЬЕРА. Здесь наложение исходного эллипса (пурпурный) и новых путей BEZIER (красный).

BEZIER

Осторожно, они точно не совпадают здесь zoom:

accuracy loss

  1. решить, сколько (n) кубиков необходимо на основе |a1-a0|

    выглядит как 16 кубиков Безьена 360 градусов вроде достаточно. Чем больше, тем выше точность ... В этом случае в результате n=3 кубики

  2. получают n+3 интерполяционные кубические контрольные точки

    каждый кубик требует 4 балла, но он отрисовываеткривая между вторым и третьим, так что останется 2 точки. Это означает, что нам нужно слегка вытянуть их за пределы диапазона a0,a1, чтобы форма не искажалась. Контрольные точки - это просто точки на эллипсе (крестики) ...

  3. для каждой кубической интерполяции создать аналог BEZIER

    просто используйте формулу из ссылки выше для преобразования между двумя кубиками.

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