enable icon indicating copy to clipboard operation
enable copied to clipboard

Arcs not drawn correctly when start angle less than end angle

Open corranwebster opened this issue 3 years ago • 5 comments
trafficstars

Observed behaviour

Drawing from π/2 to 0 (it might be arguable that Agg is correct here): Agg image

Quartz image

Drawing from π/2 to 0 with clockwise True (Agg is clearly not right, Quartz is at least consistent): Agg image

Quartz image

To reproduce

Add these to enable.gcbench.suite and run:

class draw_arc_backwards:
    def __init__(self, gc, module):
        self.gc = gc
    
    def __call__(self):
        with self.gc:
            self.gc.begin_path()
            self.gc.arc(100.0, 100.0, 30.0, np.pi / 2, 0.0)
            self.gc.set_fill_color((0.33, 0.66, 0.99, 1.0))
            self.gc.fill_path()


class draw_arc_backwards_clockwise:
    def __init__(self, gc, module):
        self.gc = gc
    
    def __call__(self):
        with self.gc:
            self.gc.begin_path()
            self.gc.arc(100.0, 100.0, 30.0, np.pi / 2, 0.0, True)
            self.gc.set_fill_color((0.33, 0.66, 0.99, 1.0))
            self.gc.fill_path()

Expected behaviour

Not entirely sure.

Quartz is consistent, but has a significant discontinuity when the end angle sweeps over start angle, which may be problematic for plotting sectors and arcs (need to adjust cw flag appropriately when drawing). Agg avoids this, but the result when drawing "clockwise" doesn't make any sense at all. I suspect that the way that Quartz does it will be similar in other 2D libraries, since the Quartz call is a straight-through call to the underlying CoreGraphics routines.

A different possible interpretation is that an arc from one angle to another should be the path swept out between the angles, including windings (with associated effects on EOF vs. winding number fill, so 0 to 4π would be EOF empty), but then the purpose of the cw flag is unclear (one interpretation would be that it means angles are treated as clockwise from the x-axis).

In the end, I would be happy with being consistent with Quartz or HTML Canvas behaviour.

Whatever fix is put in place, it may impact rendering in downstream libraries.

corranwebster avatar Jul 23 '22 10:07 corranwebster

Behaviour when wrapping around is also weird.

Going from -2π to π in normal mode (this is consistent with "wrapping around"): Agg image

Quartz image

Going from -2π to π in clockwise mode: Agg image

Quartz image

corranwebster avatar Jul 23 '22 11:07 corranwebster

Parameter sweeps of arcs of all start/end points from -5π/2 to 10π/2 (inclusive), starts increasing along x-axis, ends up y axis. I am no longer convinced that Quartz is right.

Agg (normal and clockwise) kiva agg draw_arcs kiva agg draw_arcs_clockwise

Quartz (normal and clockwise) quartz draw_arcs quartz draw_arcs_clockwise

Code to replicate:

class draw_arcs:
    def __init__(self, gc, module):
        self.gc = gc
    
    def __call__(self):
        with self.gc:
            self.gc.set_fill_color((0.33, 0.66, 0.99, 1.0))
            for i in range(-5, 11):
                x = (i + 5) * 32 + 16.0
                start = i * np.pi / 2
                for j in range(-5, 11):
                    y = (j + 5) * 32 + 16.0
                    end = j * np.pi / 2
                    with self.gc:
                        self.gc.begin_path()
                        self.gc.arc(x, y, 12.0, start, end)
                        self.gc.fill_path()


class draw_arcs_clockwise:
    def __init__(self, gc, module):
        self.gc = gc
    
    def __call__(self):
        with self.gc:
            self.gc.set_fill_color((0.33, 0.66, 0.99, 1.0))
            for i in range(-5, 11):
                x = (i + 5) * 32 + 16.0
                start = i * np.pi / 2
                for j in range(-5, 11):
                    y = (j + 5) * 32 + 16.0
                    end = j * np.pi / 2
                    with self.gc:
                        self.gc.begin_path()
                        self.gc.arc(x, y, 12.0, start, end, True)
                        self.gc.fill_path()

corranwebster avatar Jul 23 '22 12:07 corranwebster

Agg's "clockwise" seems to be consistent with "subtract 2π from the start angle"

Edit: this line doesn't seem right when sweeping backwards: https://github.com/enthought/enable/blob/bb2c358248ef72c03d62b2563985f01e1dad6977/kiva/agg/src/kiva_compiled_path.cpp#L84

corranwebster avatar Jul 23 '22 12:07 corranwebster

Decision, following discussion:

  • start and end angles represent points on the circle
  • clockwise/anti-clockwise determine which path between those two points

This is the behaviour that the HTML Canvas follows.

corranwebster avatar Jul 25 '22 09:07 corranwebster

If you're curious what HTML Canvas does, here's some code that can be plugged into CodePen.

HTML:

<canvas id="canvas" width="1000" height="1000"></canvas>

JS:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#55aaff";

const idxs = Array.from({ length: 16 }, (_, x) => x - 5);
idxs.map((i) => {
  const x = (i + 5) * 32 + 16.0;
  const start = (i * Math.PI) / 2;
  idxs.map((j) => {
    const y = (j + 5) * 32 + 16.0;
    const end = (j * Math.PI) / 2;

    ctx.beginPath();
    ctx.arc(x, y, 12.0, start, end); // A final argument of `true` can be added here
    ctx.fill();
  });
});

Which gives this output: Screen Shot 2022-07-28 at 18 06 38

jwiggins avatar Jul 28 '22 16:07 jwiggins