javascriptdelphisvgconverterstcanvas

How to convert TCanvas->Arc value to SVG Arc


I write a converter for my Company from Metafile to SVG (TCanvas->arc). I already finished to convert rectangle or some other elements but i dont get it how i can convert the arc. I write my Code in JavaScript. :)

I have a file and i read it in buffer and get the values but that is uninteresting for you.

So we currently have all the values I can get: Point1,Point2,Start,End

These 4 points are given and from this I should draw an arc now

    dc->Arc (Point1.x + offset->x,
             Point1.y + offset->y, 
             Point2.x + offset->x,
             Point2.y + offset->y,
              Start.x + offset->x,
              Start.y + offset->y, 
              Ende.x + offset->x, 
              Ende.y + offset->y);

They are currently drawing the arc with this command. You can not pay attention to the offset here.

How can i get all Informations from my given points to draw in Arc in SVG.

for Example real values:

         Point1: -50, -6
         Point2: -10, 34
         Start:  -10, 34
         End:    -10, -6 

or

         Point1:  1, 18
         Point2: 41, 58
         Start:  1, 18
         End:    1, 58 

How do I get to the: large-arc-flag, sweep-flag and rotation and what values do I have to use or calculate that it is drawn correctly.

I tried to draw it and looked at a lot of documentation and tried to create it in writing.


Solution

  • I've whipped up something that seems to work. It's based on the documentation here.

    I haven't tested it exhaustively.

    I've made the assumption that, in a TCanvas, (0,0) is at the top. If it isn't, you'll need to reverse the logic of the sweep and large arc flags.

    var svg = document.querySelector("svg");
    var debug = svg.getElementById("debug");
    
    
    function arc(x1, y1, x2, y2, x3, y3, x4, y4)
    {
      let xRadius = Math.abs(x2 - x1) / 2;
      let yRadius = Math.abs(y2 - y1) / 2;
      let xCentre = Math.min(x1, x2) + xRadius;
      let yCentre = Math.min(y1, y2) + yRadius;
    
      // get intercepts relative to ellipse centre
      let startpt = interceptEllipseAndLine(xRadius, yRadius, x3 - xCentre, y3 - yCentre);
      let endpt = interceptEllipseAndLine(xRadius, yRadius, x4 - xCentre, y4 - yCentre);
      let largeArcFlag = isLargeArc(startpt, endpt) ? 1 : 0;
    
      return ['M', xCentre + startpt.x, yCentre + startpt.y,
              'A', xRadius, yRadius, 0, largeArcFlag, 0, xCentre + endpt.x, yCentre + endpt.y].join(' ');
    }
    
    // Finds the intercept of an ellipse and a line from centre to x0,y0
    function interceptEllipseAndLine(xRadius, yRadius, x0,y0)
    {
      let den = Math.sqrt(xRadius * xRadius * y0 * y0 + yRadius * yRadius * x0 * x0);
      let mult = xRadius * yRadius / den;
      return {x: mult * x0, y: mult * y0};
    }
    
    // Returns true if the angle between the two intercept lines is >= 180deg
    function isLargeArc(start, end)
    {
      let angle = Math.atan2(start.x * end.y - start.y * end.x, start.x * end.x + start.y * end.y);
      return angle > 0; 
    }
    
    
    let path1 = svg.getElementById("path1");
    path1.setAttribute("d", arc(1, 18, 41, 58, 1, 18, 1, 58) );
    
    let path2 = svg.getElementById("path2");
    path2.setAttribute("d", arc(-50, -6, -10, 34, -10, 34, -10, -6) );
    svg {
      width: 400px;
    }
    
    path {
      fill: none;
      stroke: red;
      stroke-width: 1px;
    }
    <svg viewBox="-100 -100 200 200">
      <path id="path1"/>
      <path id="path2"/>
    </svg>

    And here's a version that adds some extra shapes for debugging purposes...

    var svg = document.querySelector("svg");
    var debug = svg.getElementById("debug");
    
    
    function arc(x1, y1, x2, y2, x3, y3, x4, y4)
    {
      let xRadius = Math.abs(x2 - x1) / 2;
      let yRadius = Math.abs(y2 - y1) / 2;
      let xCentre = Math.min(x1, x2) + xRadius;
      let yCentre = Math.min(y1, y2) + yRadius;
    
      {
        let rect = document.createElementNS(svg.namespaceURI, "rect");
        rect.setAttribute("x", x1);
        rect.setAttribute("y", y1);
        rect.setAttribute("width", x2-x1);
        rect.setAttribute("height", y2-y1);
        debug.append(rect);
    
        let ellipse = document.createElementNS(svg.namespaceURI, "ellipse");
        ellipse.setAttribute("cx", xCentre);
        ellipse.setAttribute("cy", yCentre);
        ellipse.setAttribute("rx", xRadius);
        ellipse.setAttribute("ry", yRadius);
        debug.append(ellipse);
    
        let start = document.createElementNS(svg.namespaceURI, "line");
        start.setAttribute("x1", xCentre);
        start.setAttribute("y1", yCentre);
        start.setAttribute("x2", x3);
        start.setAttribute("y2", y3);
        debug.append(start);
    
        let end = document.createElementNS(svg.namespaceURI, "line");
        end.setAttribute("x1", xCentre);
        end.setAttribute("y1", yCentre);
        end.setAttribute("x2", x4);
        end.setAttribute("y2", y4);
        debug.append(end);
      }
    
      // get intercepts relative to ellipse centre
      let startpt = interceptEllipseAndLine(xRadius, yRadius, x3 - xCentre, y3 - yCentre);
      let endpt = interceptEllipseAndLine(xRadius, yRadius, x4 - xCentre, y4 - yCentre);
      let largeArcFlag = isLargeArc(startpt, endpt) ? 1 : 0;
    
      {
        let circ = document.createElementNS(svg.namespaceURI, "circle");
        circ.setAttribute("cx", xCentre + startpt.x);
        circ.setAttribute("cy", yCentre + startpt.y);
        circ.setAttribute("r", 1);
        debug.append(circ);
      }
    
      return ['M', xCentre + startpt.x, yCentre + startpt.y,
              'A', xRadius, yRadius, 0, largeArcFlag, 0, xCentre + endpt.x, yCentre + endpt.y].join(' ');
    }
    
    // Finds the intercept of an ellipse and a line from centre to x0,y0
    function interceptEllipseAndLine(xRadius, yRadius, x0,y0)
    {
      let den = Math.sqrt(xRadius * xRadius * y0 * y0 + yRadius * yRadius * x0 * x0);
      let mult = xRadius * yRadius / den;
      return {x: mult * x0, y: mult * y0};
    }
    
    // Returns true if the angle between the two intercept lines is >= 180deg
    function isLargeArc(start, end)
    {
      let angle = Math.atan2(start.x * end.y - start.y * end.x, start.x * end.x + start.y * end.y);
      return angle > 0; 
    }
    
    
    let path1 = svg.getElementById("path1");
    path1.setAttribute("d", arc(1, 18, 41, 58, 1, 18, 1, 58) );
    
    let path2 = svg.getElementById("path2");
    path2.setAttribute("d", arc(-50, -6, -10, 34, -10, 34, -10, -6) );
    svg {
      width: 400px;
    }
    
    ellipse, rect, line {
      fill: none;
      stroke: lightgrey;
      stroke-width: 0.5px;
    }
    
    path {
      fill: none;
      stroke: red;
      stroke-width: 1px;
    }
    <svg viewBox="-100 -100 200 200">
      <g id="debug"></g>
      
      <path id="path1"/>
      <path id="path2"/>
    </svg>

    Update: Pie

    For the Pie function, it should be almost identical to arc() but it will return a slightly different path.

    function pie(x1, y1, x2, y2, x3, y3, x4, y4)
    {
      // ... rest of function is the same as arc() ...
      return ['M', xCentre, yCentre,
              'L', xCentre + startpt.x, yCentre + startpt.y,
              'A', xRadius, yRadius, 0, largeArcFlag, 0, xCentre + endpt.x, yCentre + endpt.y,
              'Z'].join(' ');
    }