7

Ok so granted, its not a bug, but I am confounded by how to get a perfect circle arc between points via Bézier curve.

I need a shape like this:

enter image description here

So I've been calculating the four corner points like this from the center point, radius and angle with the following formula: (x?,y?)=(x+d cos α,y+d sin α), which in my coffeescript looks something like this:

x1 = centerPointX+outerRadius*Math.cos(currentAngle)
y1 = centerPointY+outerRadius*Math.sin(currentAngle)
x2 = centerPointX+innerRadius*Math.cos(currentAngle)
y2 = centerPointY+innerRadius*Math.sin(currentAngle)
x3 = centerPointX+outerRadius*Math.cos(currentAngle2)
y3 = centerPointY+outerRadius*Math.sin(currentAngle2)
x4 = centerPointX+innerRadius*Math.cos(currentAngle2)
y4 = centerPointY+innerRadius*Math.sin(currentAngle2)

How can I take the information I have and result in a path element with perfect circular curves?

(PS I am newish to SVG and if you want to help me out with the proper syntax for d= that would be cool, but I can always just write it myself. The challenge I would like help with is really more to do with Bézier.

UPDATE / SOLUTION

Using the answer below a guidance below is the function I actually used:

annularSector = (centerX,centerY,startAngle,endAngle,innerRadius,outerRadius) ->               
    startAngle  = degreesToRadians startAngle+180
    endAngle    = degreesToRadians endAngle+180
    p           = [ 
        [ centerX+innerRadius*Math.cos(startAngle),     centerY+innerRadius*Math.sin(startAngle) ]
        [ centerX+outerRadius*Math.cos(startAngle),     centerY+outerRadius*Math.sin(startAngle) ]
        [ centerX+outerRadius*Math.cos(endAngle),       centerY+outerRadius*Math.sin(endAngle) ]
        [ centerX+innerRadius*Math.cos(endAngle),       centerY+innerRadius*Math.sin(endAngle) ] 
    ]
    angleDiff   = endAngle - startAngle
    largeArc    = (if (angleDiff % (Math.PI * 2)) > Math.PI then 1 else 0)
    commands    = []

    commands.push "M" + p[0].join()
    commands.push "L" + p[1].join()
    commands.push "A" + [ outerRadius, outerRadius ].join() + " 0 " + largeArc + " 1 " + p[2].join()
    commands.push "L" + p[3].join()
    commands.push "A" + [ innerRadius, innerRadius ].join() + " 0 " + largeArc + " 0 " + p[0].join()
    commands.push "z"

    return commands.join(" ")   
Fresheyeball
  • 29,567
  • 20
  • 102
  • 164
  • To be sure, you need a path outlining that shape, correct? Having a single arc along the enter with a very wide and capped linestroke does not suffice...right? – Phrogz Jul 13 '12 at 23:03
  • Also, note that this [is not possible](http://stackoverflow.com/questions/1734745/how-to-create-circle-with-bezier-curves) _exactly_ using Bézier quadratic bezier handles only. It can be done exactly using the SVG [arc command](http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands) however. Do you want a Bézier approximation or an exact arc-based solution? – Phrogz Jul 13 '12 at 23:06
  • Arc works too! As long as its the outline. – Fresheyeball Jul 14 '12 at 06:39

1 Answers1

19

Demo: http://phrogz.net/svg/procedural_annular_sector.xhtml

Usage:

annularSector( myPathElement, {
  centerX:100, centerY:150,
  startDegrees:190, endDegrees:230,
  innerRadius:75, outerRadius:100
});

Core function:

// Options:
// - centerX, centerY: coordinates for the center of the circle    
// - startDegrees, endDegrees: fill between these angles, clockwise
// - innerRadius, outerRadius: distance from the center
// - thickness: distance between innerRadius and outerRadius
//   You should only specify two out of three of the radii and thickness
function annularSector(path,options){
  var opts = optionsWithDefaults(options);
  var p = [ // points
    [opts.cx + opts.r2*Math.cos(opts.startRadians),
     opts.cy + opts.r2*Math.sin(opts.startRadians)],
    [opts.cx + opts.r2*Math.cos(opts.closeRadians),
     opts.cy + opts.r2*Math.sin(opts.closeRadians)],
    [opts.cx + opts.r1*Math.cos(opts.closeRadians),
     opts.cy + opts.r1*Math.sin(opts.closeRadians)],
    [opts.cx + opts.r1*Math.cos(opts.startRadians),
     opts.cy + opts.r1*Math.sin(opts.startRadians)],
  ];

  var angleDiff = opts.closeRadians - opts.startRadians;
  var largeArc = (angleDiff % (Math.PI*2)) > Math.PI ? 1 : 0;
  var cmds = [];
  cmds.push("M"+p[0].join());                                // Move to P0
  cmds.push("A"+[opts.r2,opts.r2,0,largeArc,1,p[1]].join()); // Arc to  P1
  cmds.push("L"+p[2].join());                                // Line to P2
  cmds.push("A"+[opts.r1,opts.r1,0,largeArc,0,p[3]].join()); // Arc to  P3
  cmds.push("z");                                // Close path (Line to P0)
  path.setAttribute('d',cmds.join(' '));

  function optionsWithDefaults(o){
    // Create a new object so that we don't mutate the original
    var o2 = {
      cx           : o.centerX || 0,
      cy           : o.centerY || 0,
      startRadians : (o.startDegrees || 0) * Math.PI/180,
      closeRadians : (o.endDegrees   || 0) * Math.PI/180,
    };

    var t = o.thickness!==undefined ? o.thickness : 100;
    if (o.innerRadius!==undefined)      o2.r1 = o.innerRadius;
    else if (o.outerRadius!==undefined) o2.r1 = o.outerRadius - t;
    else                                o2.r1 = 200           - t;
    if (o.outerRadius!==undefined)      o2.r2 = o.outerRadius;
    else                                o2.r2 = o2.r1         + t;

    if (o2.r1<0) o2.r1 = 0;
    if (o2.r2<0) o2.r2 = 0;

    return o2;
  }
}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Man you went overboard! Thank you! – Fresheyeball Jul 14 '12 at 18:08
  • 1
    @Fresheyeball You're welcome. Note that I just updated the demo and answer to be slightly cleaner. (Re-order the points so that the final `closePath` command draws one of the straight lines, and use `array.join()` instead of string concatenation to get code that is more efficient, less characters, and more clear. – Phrogz Jul 16 '12 at 16:01
  • Cool, you should also check that end is greater than start, (ie. add 360degrees to end if it's less.) e.g. `var _end = (end < start) ? (Math.PI*2) + end : end;` – ocodo Nov 12 '12 at 12:31
  • (it's probably worth checking how much greater start is than end, or just normalise them to (ie. angle % Math.PI*2) first.) – ocodo Nov 12 '12 at 14:13