7
votes

In svg I want to build a function which returns all parameters for an "arc element" in a path-d attribute. Given a start point, end point and a via-point. (3 points on a unstraight line are per definition on a circle). I'm only interested in circle-arcs (rx == ry).

I can calculate the center, and the radius quite easily. But I'm struggling with the 2 flags, is there a sharp definition how to set these flags by comparing the topology of the 3 points? like angles or distances to each other?)

I'm aware of the meaning of the flags, smallest vs largest arc, clockwise vs anti clock wise for the sweep flag.

2

2 Answers

8
votes

Yes, you can determine the large-arc and sweep flags of an svg path arc segment by looking at the angles formed by the start (S), via (V) and end (E) points of that arc.

For the large arc flag:

  • If |∠SVE| > π/2 then flag = 0
  • If |&angle;SVE| < π/2 then flag = 1
    • You are determining whether the angle centred on V is acute or obtuse.
    • If that angle is "pointy" (i.e. acute), then you'll be going the long way around the circle.

For the sweep flag:

  • If &angle;ESV > 0 then flag = 0
  • If &angle;ESV < 0 then flag = 1
    • You are determining which side of the S-to-E line the V point is on.
    • When you look from S toward E, if V is to the right, then you'll be moving counter-clockwise around the circle.

Note the following:

  • The order of the points making the angles matters: S-V-E versus E-S-V for the large arc flag and the sweep flag respectively.
  • All angles must be between -π and π, i.e. between -180° and 180°.
  • An absolute value is used to determine the large arc flag but not the sweep flag.

The demo code at the bottom of this answer demonstrates these calculations. The only code important for directly answering the OP's question are the first two lines:

const lgArcFl = (S,V,E) => Math.abs(angle(S,V,E)) > pi/2 ? 0 : 1;
const sweepFl = (S,V,E) =>          angle(E,S,V)  > 0    ? 0 : 1;

To use the demo, click the "Run code snippet" button and then click 3x on the rectangle to position the start (S), via (V) and end (E) points, in that order. The demo will calculate the radius and then draw all 4 arcs possible with the various combinations of the two flags, i.e. 0,0, 0,1, 1,0 and 1,1. The large and small arcs will be blue and red respectively while the clockwise and counter-clockwise arcs will be solid and dotted respectively. The one correct arc will be highlighted in yellow. Once the arcs are drawn you can re-click 3x to repeat, etc. Note that clicking twice in the same place or clicking 3x in a straight line won't produce any arcs, as would be expected geometrically.

Note that, as of my initial posting of this answer (Nov 5, 2016), the demo works in Chrome, Opera and Safari but not in Firefox. (I haven't checked Explorer or Edge, nor any mobile browsers.) I suspect that this may be because I used ES6/ES2015 code. However, clicking the "Use BabelJS / ES2015" button in the code editor somehow makes the code unuseable for reasons I do not understand. So, if you're having trouble getting the demo to work, perhaps try a different browser.

const lgArcFl = (S,V,E) => Math.abs(angle(S,V,E)) > pi/2 ? 0 : 1;
const sweepFl = (S,V,E) =>          angle(E,S,V)  > 0    ? 0 : 1;

const angle = ([a,b],[c,d],[e,f]) => (Math.atan2(f-d,e-c)-Math.atan2(b-d,a-c)+3*pi)%(2*pi)-pi;
const qs = sel => document.querySelector(sel), pi = Math.PI, pts = [];
const radius = ([a,b],[c,d],[e,f]) => {
  const g=c-a,h=2*(c-e)/g,i=d-b,j=c*c+d*d,k=j-a*a-b*b,l=(j-e*e-f*f-h*k/2)/(2*(d-f)-h*i);
  return Math.hypot(a+(i*l-k/2)/g,b-l);
};
const mkArc = (arc, [sx, sy], [ex, ey], r, lg, sw) => arc.setAttribute('d',
  `M ${sx} ${sy} A ${r} ${r} 0 ${lg} ${sw} ${ex} ${ey}`);
const calcArcs = (S,V,E) => {
  const args = [S, E, radius(S,V,E)];
  [[0,0],[0,1],[1,0],[1,1]].forEach(([lg,sw]) => mkArc(qs(`#arc${lg}${sw}`), ...args, lg, sw));
  mkArc(qs(`#arc`), ...args, lgArcFl(S,V,E), sweepFl(S,V,E));
};
let ptNum = 0;
qs('svg').addEventListener('click', evt => {
  const x = evt.x - 10, y = evt.y - 10;
  pts[ptNum] = [x, y];
  qs('#pt' + ptNum).setAttribute('transform', `translate(${x},${y})`);
  if (ptNum++ === 2) {
    calcArcs(...pts);
    ptNum = 0;
  }
});
text {
  font-family: courier;
  font-size: 18px;
  fill: black;
  stroke: none;
  transform: translate(-5px,5px);
}
#arc00, #arc01 {
  stroke: red;
}
#arc10, #arc11 {
  stroke: blue;
}
#arc00, #arc10 {
  stroke-width: 4;
  stroke-dasharray: 5,5;
}
#arc01, #arc11 {
  stroke-width: 2;
}
rect {
  fill: none;
  stroke: black;
  height: 200px;
  width: 600px;
}
<svg height="200" width="600">
  <defs>
    <path id="arc" />
    <circle id="circ" cx="0" cy="0" r="10" />
  </defs>
  <g fill="none">
    <use xlink:href="#arc" stroke="black" stroke-width="12" />
    <use xlink:href="#arc" stroke="#ff8"  stroke-width="10" />
    <path id="arc00" />
    <path id="arc01" />
    <path id="arc10" />
    <path id="arc11" />
  </g>
  <g fill="#ff8" stroke="black">
    <g id="pt0" transform="translate(-99,0)"><use xlink:href="#circ" /><text>S</text></g>
    <g id="pt1" transform="translate(-99,0)"><use xlink:href="#circ" /><text>V</text></g>
    <g id="pt2" transform="translate(-99,0)"><use xlink:href="#circ" /><text>E</text></g>
  </g>
  <rect />
</svg>
2
votes

I recently did some work on svg circular arcs and used the following to get the arc sweep and d values. This may be helpful.

//---x1,y1 and x2,y2 are the two end points---
    function polarToCartesian(centerX, centerY,radius, angleInDegrees)
    {
        var angleInRadians = (angleInDegrees) * Math.PI / 180.0;

        return {
        x: centerX + (radius * Math.cos(angleInRadians)),
        y: centerY + (radius * Math.sin(angleInRadians))
        };
    }
    var startAngle = 180/Math.PI*Math.atan2(y1-cy, x1-cx);
    var endAngle =  180/Math.PI*Math.atan2(y2-cy, x2-cx);

    StartPnt = polarToCartesian(cx, cy, radius, startAngle);
    EndPnt = polarToCartesian(cx, cy,  radius, endAngle);
    ArcSweep = endAngle - startAngle <= 180 ? "0" : "1";

    var d = [
    "M", StartPnt.x, StartPnt.y,
    "A", radius, radius, 0, ArcSweep, 0, EndPnt.x, EndPnt.y
    ].join(" ");