svg arc, how to determine sweep and larg-arc flags given start end & via point
Asked Answered
K

2

11

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.

Keys answered 16/2, 2014 at 20:40 Comment(0)
R
12

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 |∠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 ∠ESV > 0 then flag = 0
  • If ∠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>
Resting answered 6/11, 2016 at 0:20 Comment(0)
L
1

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(" ");
Lexical answered 16/2, 2014 at 22:47 Comment(1)
This is not correct. SVG arc is "A radx rady xrot-deg largearc sweep endx endy". What you are calling ArcSweep is actually the largeArc flag, which is '1' if the angle is > PI. The sweep flag determines whether is drawn clockwise or CCW and has the effect of reflecting the arc about the line between start/end.Accomplished

© 2022 - 2024 — McMap. All rights reserved.