manim icon indicating copy to clipboard operation
manim copied to clipboard

Circular arcs are inaccurate for large angles

Open zahlman opened this issue 2 years ago • 2 comments

Description of bug / unexpected behavior

When creating Arcs for large angles, the control point calculation gives noticeably inaccurate results. Even for the built-in circle drawn with 8 arcs anchored on the circle, e.g. the point at theta = tau/16 (halfway through the first arc) falls short by more than 1% by my calculation.

The calculation of the control points is to blame. The formula can be improved to give much more accurate circular arcs.

In `manim.mobject.geometry.arc`, the internal calculation of control points looks like (click to expand):
def _set_pre_positioned_points(self):
    anchors = np.array(
        [
            np.cos(a) * RIGHT + np.sin(a) * UP
            for a in np.linspace(
                self.start_angle,
                self.start_angle + self.angle,
                self.num_components,
            )
        ],
    )
    # Figure out which control points will give the
    # Appropriate tangent lines to the circle
    d_theta = self.angle / (self.num_components - 1.0)
    tangent_vectors = np.zeros(anchors.shape)
    # Rotate all 90 degrees, via (x, y) -> (-y, x)
    tangent_vectors[:, 1] = anchors[:, 0]
    tangent_vectors[:, 0] = -anchors[:, 1]
    # Use tangent vectors to deduce anchors
    handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1]
    handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:]
    self.set_anchors_and_handles(anchors[:-1], handles1, handles2, anchors[1:])

The d_theta / 3 factor on the second-last and third-last lines should instead be 4/3 * np.tan(d_theta/4). This value approaches d_theta / 3 in the limit as d_theta goes to 0, but is way off for larger values. The resulting approximation aligns the midpoint of the Bezier curve with the corresponding circular arc.

Citations for this calculation:

https://stackoverflow.com/questions/1734745 https://spencermortensen.com/articles/bezier-circle/

As noted in the latter link, the result can be improved further; however, the benefits are much smaller, the necessary control point values are quite close, and there is no explicit formula given (only the results for an angle of tau/4). (Also, seeking the absolute best possible approximation in terms of the absolute deviation from the circle, will result in cusps at the handle points.)

Expected behavior

Circular arcs should appear circular even for large spans, and should not require subdivision into smaller arcs to achieve a good approximation. The reproduction code below results in a noticeably compressed ellipse (by a factor of pi/4, in fact).

How to reproduce the issue

from manim import *
c = Circle(num_components = 3) # two arcs
Scene().add(c).render(preview=True)

Additional media files

Images/GIFs

I get a result like so:

Scene_ManimCE_v0 17 3

With an improved calculation, much better results are possible. Here are examples from my own code, working directly in Cairo, showing the results with Manim's formula vs. the improved formula (for a circle made with three arcs). The shape goes from noticeably "wobbly" to indistinguishable from correct (at least at 256x256 circle size).

bad good

zahlman avatar Aug 27 '23 16:08 zahlman

Perhaps @chopan050 may be interested in this, in light of recent pull requests.

zahlman avatar Aug 27 '23 19:08 zahlman

Using 4/3 * np.tan(d_theta/4) seems pretty fine to me! :+1:

Going even further, as suggested by the other methods in the pages you linked, is very complex and impractical. Plus, as you mentioned, it results in much smaller gains which aren't really noticeable.

chopan050 avatar Aug 27 '23 22:08 chopan050