Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite manim.utils.bezier.get_quadratic_approximation_of_cubic() to produce curves which can be animated smoothly #3829

Merged
merged 3 commits into from
Jul 13, 2024

Conversation

chopan050
Copy link
Contributor

@chopan050 chopan050 commented Jun 27, 2024

Overview: What does this pull request change?

  • What the title says
  • Added a new test for this function

Motivation and Explanation: Why and how do your changes improve the library?

The current implementation splits the cubic Bézier at its inflection point (if it exists, otherwise simply t = 0.5). This sounds quite right: the curvature changes at the inflection point, something which can't be captured by a single quadratic Bézier, so we split the curve into two at that point in order to approximate each curvature with different curves.

However:

BezierScene.mp4

As you can see, the speed of the point (1st derivative of the Béziers) is discontinuous when transitioning from one quadratic to the other.

This is problematic for 2 reasons:

  • An animation like Create, Write or DrawBorderThenFill won't draw the resultant VMobject as smoothly as it could (the drawing speed might suddenly change)... even if it the original cubic spline was actually smooth.
  • OpenGL uses quadratic Béziers whereas Cairo uses cubic Béziers. When OpenGLVMobject.make_smooth() is called, it interpolates the curve anchors with a smooth cubic spline, and then proceeds to approximate it with quadratic Béziers by calling get_quadratic...(). So, OpenGLVMobject.make_smooth() promises to transform the Mobject into a smooth curve, but get_quadratic...() breaks that promise.

Therefore I propose an alternate implementation: instead of forcing a split at a specific point on the curve, I explicitly require that the curves and their 1st derivatives are continuous. (See the docstring of get_quadratic...() for more details on the mathematical process)

With this implementation, the result is as follows:

BezierScene.mp4

Another example with more exotic curves (the spline is, however, not smooth, so the speeds at the green points aren't continuous:

  • Before:
BezierScene.mp4
  • After:
BezierScene.mp4

Notice that the resultant curve might be slightly more off than the original approximation. The original seems more proper for static images where it's enough that the tangents are continuous (the speed directions are the same, rather than the speeds themselves). However, Manim is mainly an animation library, so it's necessary that the speeds are also continuous in this case.

Links to added or changed documentation pages

https://manimce--3829.org.readthedocs.build/en/3829/reference/manim.utils.bezier.html#manim.utils.bezier.get_quadratic_approximation_of_cubic

Further Information and Comments

The code I used:

from manim import *
from manim.utils.bezier import get_quadratic_approximation_of_cubic
from manim.typing import CubicBezierPoints

class BezierScene(Scene):
    def construct(self):
        base = VMobject().set_points_as_corners([
            [-5, 2, 0],
            [-2, 2, 0],
            [-3, 0, 0],
            [5, -3, 0],
        ]).make_smooth()
        
        # Change the contents to whatever CubicBezierPoints you want
        cubic_bezier_points: list[CubicBezierPoints] = [
            base.points[i:i+4]
            for i in range(0, len(base.points), 4)
            # COMMENT THE TWO LINES ABOVE AND UNCOMMENT THE ARRAYS BELOW FOR THE 2ND EXAMPLE
            # np.array([
            #     [-5, -1, 0],
            #     [-5, 2, 0],
            #     [-3, 2, 0],
            #     [-2, -2, 0],
            # ]),
            # np.array([
            #     [-2, -2, 0],
            #     [2, -2, 0],
            #     [-2, 4, 0],
            #     [2, 2, 0],
            # ]),
            # np.array([
            #     [2, 2, 0],
            #     [6, -2, 0],
            #     [0, -2, 0],
            #     [5, 3, 0],
            # ]),
        ]
        cubic_beziers = [bezier(p) for p in cubic_bezier_points]
        cubic_derivative_points = [3 * (p[1:] - p[:-1]) for p in cubic_bezier_points]
        cubic_derivatives = [bezier(d) for d in cubic_derivative_points]

        quadratic_bezier_points = []

        # Build VMobjects curve by curve, adding separating dots
        cubic_vmob = VMobject(stroke_color=RED)
        quadratic_vmob = VMobject(stroke_color=YELLOW)
        for c in cubic_bezier_points:
            (
                cubic_vmob
                .start_new_path(c[0])
                .add_cubic_bezier_curve_to(*c[1:])
                .add(Dot(c[0], color=GREEN))
            )
            Q = get_quadratic_approximation_of_cubic(*c)
            q0, q1 = Q[:3], Q[3:]
            quadratic_bezier_points.append(q0)
            quadratic_bezier_points.append(q1)
            (
                quadratic_vmob
                .start_new_path(q0[0])
                .add_quadratic_bezier_curve_to(*q0[1:])
                .add_quadratic_bezier_curve_to(*q1[1:])
                .add(Dot(q0[0], color=GREEN), Dot(q1[0], color=BLUE))
            )
        cubic_vmob.add(Dot(cubic_bezier_points[-1][-1], color=GREEN))
        quadratic_vmob.add(Dot(cubic_bezier_points[-1][-1], color=GREEN))
        
        quadratic_beziers = [bezier(p) for p in quadratic_bezier_points]
        quadratic_derivative_points = [2 * (p[1:] - p[:-1]) for p in quadratic_bezier_points]
        quadratic_derivatives = [bezier(d) for d in quadratic_derivative_points]

        t = ValueTracker(0) # between 0 and 3

        def dot_cubic_updater(dot: Dot):
            val = t.get_value()
            i, alpha = integer_interpolate(0, 3, val) if val < 3 else (2, 1.0)
            B = cubic_beziers[i]
            dot.move_to(B(alpha))
            
        def arrow_cubic_updater(arrow: Arrow) -> Arrow:
            val = t.get_value()
            i, alpha = integer_interpolate(0, 3, val) if val < 3 else (2, 1.0)
            B = cubic_beziers[i]
            dB = cubic_derivatives[i]
            arrow.put_start_and_end_on(B(alpha), B(alpha) + dB(alpha)/2)

        dot = Dot(color=WHITE, radius=0.15).add_updater(dot_cubic_updater)
        arrow = Arrow(color=WHITE, buff=0.0).add_updater(arrow_cubic_updater)

        self.wait(0.5)
        self.play(FadeIn(cubic_vmob))

        self.play(FadeIn(dot, arrow))
        self.play(t.animate.set_value(1), run_time=6, rate_func=linear)
        self.play(FadeOut(dot, arrow))

        dot.clear_updaters()
        arrow.clear_updaters()
        
        def dot_quadratic_updater(dot: Dot):
            val = t.get_value()
            i, alpha = integer_interpolate(0, 6, val) if val < 6 else (5, 1.0)
            B = quadratic_beziers[i]
            dot.move_to(B(alpha))
            
        def arrow_quadratic_updater(arrow: Arrow) -> Arrow:
            val = t.get_value()
            i, alpha = integer_interpolate(0, 6, val) if val < 6 else (5, 1.0)
            B = quadratic_beziers[i]
            dB = quadratic_derivatives[i]
            arrow.put_start_and_end_on(B(alpha), B(alpha) + dB(alpha)/2)

        dot.add_updater(dot_quadratic_updater)
        arrow.add_updater(arrow_quadratic_updater)

        t.set_value(0)
        self.play(FadeIn(quadratic_vmob))

        self.play(FadeIn(dot, arrow))
        self.play(t.animate.set_value(1), run_time=12, rate_func=linear)
        self.play(FadeOut(dot, arrow))

Reviewer Checklist

  • The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • If applicable: newly added functions and classes are tested

manim/utils/bezier.py Dismissed Show dismissed Hide dismissed
manim/utils/bezier.py Dismissed Show dismissed Hide dismissed
@JasonGrace2282 JasonGrace2282 added the enhancement Additions and improvements in general label Jul 9, 2024
Copy link
Member

@behackl behackl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved behavior, better tests -- what is not to like?

Thanks for your efforts!

@behackl behackl merged commit 1df1709 into ManimCommunity:main Jul 13, 2024
19 checks passed
@chopan050 chopan050 deleted the rewrite-quadratic-approx branch July 14, 2024 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Additions and improvements in general
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants