[cairo] Shouldn't Cairo use/offer degrees rather than radians?

David Kastrup dak at gnu.org
Tue Jun 27 11:18:15 UTC 2017


David Kastrup <dak at gnu.org> writes:

> Lawrence D'Oliveiro <ldo at geek-central.gen.nz> writes:
>
>> On Tue, 27 Jun 2017 10:24:18 +0200, Guillermo Rodriguez wrote:
>>
>>> 2017-06-27 9:57 GMT+02:00 Lawrence D'Oliveiro:
>>>
>>>> On Tue, 27 Jun 2017 08:03:36 +0200, David Kastrup wrote:
>>>>  
>>>>> PostScript uses degrees.  PDF uses degrees.  
>>>>
>>>> No!  
>>> 
>>> Actually, yes..
>>
>> Trig calculations are usually easier in radians.
>
> Nobody uses Taylor series for actual trig calculations.  The Cordic
> algorithms work from full revolutions.
>
> If I write
>
> #include "math.h"
> #include "stdio.h"
>
> int
> main ()
> {
>   printf ("%lg\n", cos (M_PI/2));
>   return 0;
> }
>
> and run it, the output is 6.12323e-17 .  So apparently radians do not
> make things easy enough for computer/user to work well in graphics
> applications.
>
> If you want to turn something by exactly 90 degrees or 180 degrees (not
> exactly a rare event in graphics), you cannot do so by using a Cairo
> function accepting angle arguments but have to specify the transform
> matrix manually.
>
> That is one important reason that graphics formats like SVG, PostScript
> and PDF specify angles in degrees: that way right angles are actually
> representable.

A more relevant example:

>From test/get-path-extents.c:

    cairo_rectangle (cr2, -50, -50, 50, 50);
    cairo_rotate (cr2, -M_PI/4);
    /* the path in user space is now (nearly) the square rotated by
       45 degrees about the origin. Thus its x1 and x2 are both nearly 0.
       This should show any bugs where we just transform device-space
       x1,y1 and x2,y2 to get the extents. */

So what's with all the "nearly"?

In LilyPond, we use code like

Real
Offset::angle_degrees () const
{
  Real x = coordinate_a_ [X_AXIS];
  Real y = coordinate_a_ [Y_AXIS];

  // We keep in the vicinity of multiples of 45 degrees here: this is
  // where straightforward angles for straightforward angular
  // relations are most expected.  The factors of 2 employed in the
  // comparison are not really perfect for that: sqrt(2)+1 would be
  // the factor giving exact windows of 45 degrees rather than what we
  // have here.  It's just that 2 is likely to generate nicer code
  // than 2.4 and the exact handover does not really matter.
  //
  // Comparisons here are chosen appropriately to let infinities end
  // up in their "exact" branch.  As opposed to the normal atan2
  // function behavior, this makes "competing" infinities result in
  // NAN angles.
  if (y < 0.0)
    {
      if (2*x < -y)
        if (-x > -2*y)          // x < 0, y < 0, |x| > |2y|
          return -180 + atan2d (-y, -x);
        else if (-2*x >= -y)    // x < 0, y < 0, |y| < |2x| <= |4y|
          return -135 + atan2d (x - y, -y - x);
        else                    // y < 0, |y| >= |2x|
          return -90 + atan2d (x, -y);
      else if (x <= -2*y)       // x > 0, y < 0, |y| <= |2x| < |4y|
        return -45 + atan2d (x + y, x - y);
      // Drop through for y < 0, x > |2y|
    }
  else if (y > 0.0)
    {
      if (2*x < y)
        if (-x > 2*y)           // x < 0, y >= 0, |x| > |2y|
          return 180 - atan2d (y, -x);
        else if (-2*x >= y)     // x < 0, y >= 0, |y| < |2x| <= |4y|
          return 135 - atan2d (x + y, y - x);
        else                    // y >= 0, |y| >= |2x|
          return 90 - atan2d (x, y);
      else if (x <= 2*y)        // x >= 0, y >= 0, |y| < |2x| < |4y|
        return 45 - atan2d (x - y, x + y);
      // Drop through for y > 0, x > |2y|
    }
  else
    // we return 0 for (0,0).  NAN would be an option but is a
    // nuisance for getting back to rectangular coordinates.  Strictly
    // speaking, this argument would be just as valid for (+inf.0,
    // +inf.0), but then infinities are already an indication of a
    // problem in LilyPond.
    return (x < 0.0) ? 180 : 0;
  return atan2d (y, x);
}

This is painful, but only needs to be done once, and then angles in
degree get returned like naively expected, no "nearly" involved.

The reverse direction is

Offset
offset_directed (Real angle)
{
  if (angle <= -360.0 || angle >= 360.0)
    angle = fmod (angle, 360.0);
  // Now |angle| < 360.0, and the absolute size is not larger than
  // before, so we haven't lost precision.
  if (angle <= -180.0)
    angle += 360.0;
  else if (angle > 180.0)
    angle -= 360.0;
  // Now -180.0 < angle <= 180.0 and we still haven't lost precision.
  // We don't work with angles greater than 45 degrees absolute in
  // order to minimize how rounding errors of M_PI/180 affect the
  // result.  That way, at least angles that are a multiple of 90
  // degree deliver the expected results.
  //
  // Sign of the sine is chosen to avoid -0.0 in results.  This
  // version delivers exactly equal magnitude on x/y for odd multiples
  // of 45 degrees at the cost of losing some less obvious invariants.

  if (angle > 0)
    if (angle > 90)
      return Offset (sin ((90 - angle) * M_PI/180.0),
                     sin ((180 - angle) * M_PI/180.0));
    else
      return Offset (sin ((90 - angle) * M_PI/180.0),
                     sin (angle * M_PI/180.0));
  else if (angle < -90)
    return Offset (sin ((90 + angle) * M_PI/180.0),
                   sin ((-180 - angle) * M_PI/180.0));
  else
    return Offset (sin ((90 + angle) * M_PI/180.0),
                   sin (angle * M_PI/180.0));
}

The last sentence in the comment implies that the example in Cairo would
get along without the "nearly" qualifier.

If you want to get straightforward transform matrices, this kind of fuzz
is not avoidable, and if you do it around numeric multiples of pi/4
rather than multiples of 45, results are a lot less reliable under
arithmetic manipulations.

Of course, avoiding conversions to angles at all when possible is even
better but many use cases do convert.

If you do

git grep cairo_rotate

in the Cairo code base, there are some nonsensical rotation values like
1 or 0.6 or 0.7 (note: 0.7 is not exactly representable in floating
point either) and 0.05 (which is at least in the ballpark where one
would approximate with x in Radians).

But by far most angles specified to cairo_rotate are of the scaled M_PI
variant that are not numerically exact enough to yield a trivial
transform matrix.

Radians don't work well for this API.  The large majority of
cairo_rotate calls in Cairo's code base itself are problematic for that
reason and lead to fuzzy transform matrices.

-- 
David Kastrup



More information about the cairo mailing list