manim icon indicating copy to clipboard operation
manim copied to clipboard

Add_tip shifts the line

Open tobiasBora opened this issue 2 months ago • 1 comments

Description of bug / unexpected behavior

I wanted to use a tikz-equivalent of -| 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)

One issue is the lack of rounded corners (see https://github.com/ManimCommunity/manim/issues/4443), but the other issue is that Line.add_tip is broken as discussed in https://discord.com/channels/581738731934056449/1370706466541011055/1370728872416514141:

Indeed, the naive code:

from manim import *

class Terminology(Scene):
    def construct(self):
        # Grid
        grid = NumberPlane()
        grid.add_coordinates()
        self.add(grid)

        plainText = Text("Plaintext", font_size=30, color=YELLOW)
        plainText.to_edge(LEFT, buff=0.8)
        plainTextRect = SurroundingRectangle(plainText, color=YELLOW, buff=0.3)

        self.play(Write(plainText))
        self.play(Write(plainTextRect))

        encryptionAlgorithm = Text("Encryption\nAlgorithm", font_size=30, color=BLUE)
        encryptionAlgorithm.move_to(UP*2)
        encryptionAlgorithmRect = SurroundingRectangle(encryptionAlgorithm, color=BLUE)
        self.play(Write(encryptionAlgorithm))
        self.play(Write(encryptionAlgorithmRect))

        line1 = Line()
        line1.set_points_as_corners([
            plainTextRect.get_top(),
            [plainTextRect.get_center()[0], 2, 0],
            encryptionAlgorithmRect.get_left()
        ]).add_tip(tip_length=0.15)

        self.play(Write(line1))
        
        cipher = Text("Cipher", font_size=30, color=RED)
        cipher.to_edge(RIGHT, buff=0.8)
        cipherRect = SurroundingRectangle(cipher, color=RED, buff=0.3)

        line2 = Line()
        line2.set_points_as_corners([
            encryptionAlgorithmRect.get_right(),
            [cipherRect.get_center()[0], 2, 0],
            cipherRect.get_top()
        ]).add_tip(tip_length=0.15)
            
        self.play(Write(line2))

        self.play(Write(cipher))
        self.play(Write(cipherRect))

        self.wait()

produces

Image

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