manim icon indicating copy to clipboard operation
manim copied to clipboard

Add `rounded corners` to round lines automatically

Open tobiasBora opened this issue 2 months ago • 2 comments

Description of bug / unexpected behavior

I wanted to use a tikz-equivalent of rounded corners in manim to simply write the equivalent of the tikz code:

\draw[rounded corners,-Stealth] (0,0) -| (1,-1);

to obtain something like

Image

but it turned out that the equivalent manim code is a huge mess:

import manim.mobject.geometry.tips as tips
class ellbowarrow(Scene):
    def construct(self):
        def add_tip(
            line,
            shorten=True,
            tip_shape: type[tips.ArrowTip] | None = None,
            tip_length: float | None = None,
            tip_width: float | None = None,
        ):
            tip = line.get_unpositioned_tip(
                tip_length=tip_length,
                tip_shape = tip_shape,
                tip_width = tip_width,
            )
            tipangle = Line(tip.base, tip.tip_point).get_angle()
            angle = Line(line.point_from_proportion(0.999),line.point_from_proportion(1.0)).get_angle()
            tip.rotate(angle-tipangle,about_point=tip.base)
            tip.shift(line.get_end()-tip.tip_point)
            if shorten==True:
                points = line.get_all_points()
                points[-1]=tip.base
                line.set_points(points)
            line.add(tip)
            return line
        Line.add_tip = add_tip

        grid = NumberPlane().add_coordinates()
        self.add(grid)

        p1 = Dot([-4,2,0],color=RED)
        p2 = Dot([4,-1,0],color=BLUE)

        c1 = Dot(p1.get_center()*UP+p2.get_center()*RIGHT-0.5*RIGHT, color=YELLOW)
        c2 = Dot(p2.get_center()*RIGHT+p1.get_center()*UP-0.5*UP, color=TEAL)
        self.add(p1,p2,c1,c2)
        line1 = Line().set_points_as_corners(
            [p1.get_center(), p1.get_center()*UP+p2.get_center()*RIGHT-0.5*RIGHT]
        )
        line1.add_cubic_bezier_curve(
            p1.get_center()*UP+p2.get_center()*RIGHT-0.5*RIGHT,
            p2.get_center()*RIGHT+p1.get_center()*UP,
            p2.get_center()*RIGHT+p1.get_center()*UP,
            p2.get_center()*RIGHT+p1.get_center()*UP-.5*UP
        )
        line1.add_line_to(p2.get_center())
        line1.add_tip(shorten=False, tip_length=0.5)

        self.play(Create(line1))
        self.wait()

(thanks uwezi for the help)

Would you consider adding a way to round corners of arbitrary lines to make this kind of code significantly simpler?

tobiasBora avatar Oct 15 '25 13:10 tobiasBora

the equivalent manim code is a huge mess:

I would not call my code a "huge mess" and there are certainly other methods to achieve the desired functionality - I just did not want to spend more than 10 minutes on the problem.

I made a nicer code solution:

from manim import *

import manim.mobject.geometry.tips as tips
class LineFromPoints(Line):
    def __init__(self, pointsdirections, radius=0, **kwargs):
        super().__init__(**kwargs)
        pointsradii = [(pointsdirections[0][0],0)]
        for p0,p2 in zip(pointsdirections, pointsdirections[1:]):
            if len(p0)==3:
                r = p0[2]
            else:
                r = radius
            if len(p0)>1:
                if p0[1] == "-|":
                    p1 = p0[0]*UP + p2[0]*RIGHT
                    pointsradii.append((p1,r))
                elif p0[1] == "|-":
                    p1 = p0[0]*RIGHT + p2[0]*UP
                    pointsradii.append((p1,r))
            pointsradii.append((p2[0],r))

        self.set_points([pointsradii[0][0]])
        for p0,p1,p2 in zip(pointsradii,pointsradii[1:],pointsradii[2:]):
            hl1 = Line(p0[0],p1[0])
            hl2 = Line(p1[0],p2[0])
            hl1.scale((hl1.get_length()-p1[1])/hl1.get_length(),about_point=hl1.get_start())
            hl2.scale((hl2.get_length()-p1[1])/hl2.get_length(),about_point=hl2.get_end())
            self.add_line_to(hl1.get_end())
            if r>0:
                self.add_cubic_bezier_curve(
                    hl1.get_end(),
                    p1[0],
                    p1[0],
                    hl2.get_start()
                )
        self.add_line_to(pointsradii[-1][0])

    def add_tip(
        line,
        shorten=True,
        tip_shape: type[tips.ArrowTip] | None = None,
        tip_length: float | None = None,
        tip_width: float | None = None,
    ):
        tip = line.get_unpositioned_tip(
            tip_length=tip_length,
            tip_shape = tip_shape,
            tip_width = tip_width,
        )
        tipangle = Line(tip.base, tip.tip_point).get_angle()
        angle = Line(line.point_from_proportion(0.999),line.point_from_proportion(1.0)).get_angle()
        tip.rotate(angle-tipangle,about_point=tip.base)
        tip.shift(line.get_end()-tip.tip_point)
        if shorten==True:
            points = line.get_all_points()
            points[-1]=tip.base
            line.set_points(points)
        line.add(tip)
        return line

class testLineFromPoints1(Scene):
    def construct(self):
        p1 = Dot([-4,2,0],color=RED)
        p2 = Dot([2,-1,0],color=BLUE)
        p3 = Dot([6,+3,0],color=YELLOW)
        line1 = LineFromPoints(
            [
                (p1.get_center(),"|-"),
                (p2.get_center(),"|-",1.5),
                (p3.get_center(),""),
            ],
            radius=0.5
        )
        line2 = LineFromPoints(
            [
                (p1.get_center(),"-|"),
                (p2.get_center(),"-|"),
                (p3.get_center(),""),
            ],
            radius=0.5,
            color=GREEN
        ).add_tip()
        line3 = LineFromPoints(
            [
                (p1.get_center(),""),
                (p2.get_center(),""),
                (p3.get_center(),""),
            ],
            radius=1,
            color=ORANGE
        ).add_tip(tip_shape=StealthTip, tip_length=0.35)
        self.add(p1,p2,p3)
        self.add(line1,line2,line3)

class testLineFromPoints2(Scene):
    def construct(self):
        rad = ValueTracker(0)
        p1 = Dot([-4,2,0],color=RED)
        p2 = Dot([2,-2,0],color=BLUE)
        p3 = Dot([6,+3,0],color=YELLOW)

        line2 = always_redraw(lambda:
            LineFromPoints(
                [
                    (p1.get_center(),"-|"),
                    (p2.get_center(),"-|"),
                    (p3.get_center(),""),
                ],
                radius=rad.get_value(),
                color=GREEN
            ).add_tip()
        )
        line3 = always_redraw(lambda:
            LineFromPoints(
                [
                    (p1.get_center(),""),
                    (p2.get_center(),""),
                    (p3.get_center(),""),
                ],
                radius=rad.get_value(),
                color=ORANGE
            ).add_tip(tip_shape=StealthTip, tip_length=0.35)
        )
        self.add(p1,p2,p3)
        self.add(line2,line3)
        self.wait()
        self.play(
            rad.animate.set_value(2.5),
            rate_func=rate_functions.there_and_back,
            run_time=4
        )
        self.wait()
Image

https://github.com/user-attachments/assets/ccdbca6b-9307-4168-bb6c-42df20c06403

uwezi avatar Oct 15 '25 20:10 uwezi

Ahah sorry I wasn't criticising the quality of your code at all, I'm really grateful. My point is that such rounded lines are so common that they disearve a built-in one-line solution like in tikz instead on a long code (irrespective of its quality).

tobiasBora avatar Oct 16 '25 15:10 tobiasBora