Skip to content

Draw a line between nodes without going inside #3238

Open
@tobiasBora

Description

@tobiasBora

Description of proposed feature

I’d like a way to draw a line virtually connecting the center of two objects, while drawing only the part that is not inside the node. This is interesting e.g. if the node has no background (to see another object behind), or if we draw an arrow.

How can the new feature be used?

To connect two elements, like:

image

Additional comments

We discussed this functionality here. Abulafia and uwezi came up with some awesome hackish solutions, but it would be nice to integrate it in Manim, and clean it a bit. For instance, it does not work for boolean-produced objects without some additional things like:

        self.simpler_enveloppe = body.submobjects[0]

maybe most importantly, it does not work for arrows.

from manim import *

class MyGuy(VGroup):
    def __define_shape(self,how_malicious):
        horn = Difference(Circle(),Circle().shift(.5*UP)).scale(1.3).shift(UP).set_opacity(0)
        head = Circle().set_opacity(0)
        mouth = ArcBetweenPoints(-.3*UP+.6*LEFT, -.3*UP-.6*LEFT, angle=how_malicious*-PI/3+(1-how_malicious)*(PI/2)).set_stroke(WHITE,opacity=1).scale(.5).set_z_index(1)
        body = Union(horn.copy().scale(how_malicious * self.size_horn),head).set_stroke(interpolate_color(GREEN, RED, how_malicious),opacity=1).set_fill(
            interpolate_color(interpolate_color(GREEN,BLACK,.5), interpolate_color(RED,BLACK,.5), how_malicious),
            opacity=1)
        eye1 = Dot(color=WHITE, fill_opacity=1).shift(.3*UP+.25*LEFT).set_z_index(1)
        eye2 = Dot(color=WHITE, fill_opacity=1).shift(.3*UP+.25*RIGHT).set_z_index(1)
        eyes = VGroup(eye1, eye2, mouth)
        body.scale(self.myScale)
        eyes.scale(self.myScale)
        return (body, eyes)
    
    def __init__(self, size_horn=1, **kwargs):
        super().__init__(**kwargs)
        self.myScale = 1
        self.size_horn = size_horn
        # initialize the vmobject
        body, eyes = self.__define_shape(1)
        self.become(body)
        self.add(eyes)
        self.simpler_enveloppe = body.submobjects[0]

class Connection(Line):
    def __init__(self, mobj1, mobj2, **kwargs):
        super().__init__(mobj1, mobj2, **kwargs)
        distance = np.linalg.norm(mobj1.get_center()- mobj2.get_center())
        angle = angle_of_vector(mobj2.get_center()-mobj1.get_center())
        fake_line = (Rectangle(height=0.001, width=distance)
             .move_to(mobj1.get_center(), aligned_edge=LEFT)
             .rotate(angle, about_point=mobj1.get_center())
             )
        segment = Difference(Difference(fake_line, mobj1), mobj2)
        self.segment = segment
        if segment:
            self.set_points_by_ends(
                *self.find_end_points(segment.points)
            )
        else:
            self.set_points_by_ends(ORIGIN, ORIGIN)

    def find_end_points(self, points):
        result = []
        connected = []
        for i in range(0,len(points), 4):
            p = points[i]
            if i > 0:
                previous = points[i-1]
            else:
                previous = points[-1]
            if np.linalg.norm(p-previous) < 1e-20:
                connected.append(p)
            else:
                result.append(connected)
                connected = []
        result.append(connected)
        if len(result) == 1:
            select = 0
        else:
            select = 1
        return result[select][0], result[select][len(result[select])//2]
    
class Test(Scene):
    def construct(self):
        sq1 = MyGuy()
        circ = Star(outer_radius=2, color=RED).shift(UR+3*RIGHT)
        l = Connection(sq1.simpler_enveloppe, circ, color=BLUE)
        self.play(Create(sq1), Create(circ))
        self.play(Create(l))
        self.wait()

        l.add_updater(lambda m: m.become(Connection(sq1.simpler_enveloppe, circ, color=BLUE)))
        self.play(circ.animate.shift(2.5*DOWN+LEFT))
        self.play(circ.animate.shift(8*LEFT))
        self.play(circ.animate.shift(5*UP))
        self.play(
            Rotate(circ, 3*PI,about_point=[0,0,0],run_time=10,rate_func=linear)
        )
        self.wait(3)

        self.clear()
        sq1 = Text("C").scale(5)
        circ = Text("X").scale(5).shift(2*UP+3*RIGHT)
        l = Connection(sq1[0], circ[0], color=BLUE)
        self.play(Write(sq1), Create(circ))
        self.play(Create(l))
        self.wait()
        self.add(Dot().move_to(sq1[0].get_center()))

        l.add_updater(lambda m: m.become(Connection(sq1[0], circ[0], color=BLUE)))
        self.play(circ.animate.shift(2.5*DOWN+LEFT))
        self.play(circ.animate.shift(8*LEFT))
        self.play(circ.animate.shift(4*UP))
        self.play(circ.animate.shift(3*RIGHT))

EDIT

This code can add arrow tips:

from manim import *



class MyGuy(VGroup):
    def __define_shape(self,how_malicious):
        horn = Difference(Circle(),Circle().shift(.5*UP)).scale(1.3).shift(UP).set_opacity(0)
        head = Circle().set_opacity(0)
        mouth = ArcBetweenPoints(-.3*UP+.6*LEFT, -.3*UP-.6*LEFT, angle=how_malicious*-PI/3+(1-how_malicious)*(PI/2)).set_stroke(WHITE,opacity=1).scale(.5).set_z_index(1)
        body = Union(horn.copy().scale(how_malicious * self.size_horn),head).set_stroke(interpolate_color(GREEN, RED, how_malicious),opacity=1).set_fill(
            interpolate_color(interpolate_color(GREEN,BLACK,.5), interpolate_color(RED,BLACK,.5), how_malicious),
            opacity=1)
        eye1 = Dot(color=WHITE, fill_opacity=1).shift(.3*UP+.25*LEFT).set_z_index(1)
        eye2 = Dot(color=WHITE, fill_opacity=1).shift(.3*UP+.25*RIGHT).set_z_index(1)
        eyes = VGroup(eye1, eye2, mouth)
        body.scale(self.myScale)
        eyes.scale(self.myScale)
        return (body, eyes)
    
    def __init__(self, size_horn=1, **kwargs):
        super().__init__(**kwargs)
        self.myScale = 1
        self.size_horn = size_horn
        # initialize the vmobject
        body, eyes = self.__define_shape(1)
        self.become(body)
        self.add(eyes)
        self.simpler_enveloppe = body.submobjects[0]

class Connection(Line):
    def __init__(self, mobj1, mobj2, tip=False, **kwargs):
        super().__init__(mobj1, mobj2, **kwargs)
        distance = np.linalg.norm(mobj1.get_center()- mobj2.get_center())
        angle = angle_of_vector(mobj2.get_center()-mobj1.get_center())
        fake_line = (Rectangle(height=0.001, width=distance)
             .move_to(mobj1.get_center(), aligned_edge=LEFT)
             .rotate(angle, about_point=mobj1.get_center())
             )
        segment = Difference(Difference(fake_line, mobj1), mobj2)
        self.segment = segment
        if segment:
            start, end = self.find_end_points(segment.points)
            start, end = sorted([start, end], key=lambda p: np.linalg.norm(p-mobj1.get_center()))
            self.set_points_by_ends(start, end)
            if tip:
                self.add_tip()
        else:
            self.set_points_by_ends(ORIGIN, ORIGIN)
            self.tip = None

    def find_end_points(self, points):
        result = []
        connected = []
        for i in range(0,len(points), 4):
            p = points[i]
            if i > 0:
                previous = points[i-1]
            else:
                previous = points[-1]
            if np.linalg.norm(p-previous) < 1e-20:
                connected.append(p)
            else:
                result.append(connected)
                connected = []
        result.append(connected)
        if len(result) == 1:
            select = 0
        else:
            select = 1
        return result[select][0], result[select][len(result[select])//2]
    
class Test(Scene):
    def construct(self):
        sq1 = MyGuy()
        circ = Star(outer_radius=2, color=RED).shift(UR+3*RIGHT)
        l = Connection(sq1.simpler_enveloppe, circ, tip=True, color=BLUE)
        self.play(Create(sq1), Create(circ))
        self.play(Create(l))
        self.wait()

        l.add_updater(lambda m: m.become(Connection(sq1.simpler_enveloppe, circ, tip=True, color=BLUE)))
        self.play(circ.animate.shift(2.5*DOWN+LEFT))
        self.play(circ.animate.shift(8*LEFT))
        self.play(circ.animate.shift(5*UP))
        self.play(
            Rotate(circ, 3*PI,about_point=[0,0,0],run_time=10,rate_func=linear)
        )
        self.wait(3)

        self.clear()
        sq1 = Text("C").scale(5)
        circ = Text("S").scale(5).shift(2*UP+3*RIGHT)
        l = Connection(sq1[0], circ[0], color=BLUE)
        self.play(Write(sq1), Create(circ))
        self.play(Create(l))
        self.wait()
        self.add(Dot().move_to(sq1[0].get_center()))

        l.add_updater(lambda m: m.become(Connection(sq1[0], circ[0], color=BLUE)))
        self.play(
            Rotate(circ, 3*PI,about_point=[0,0,0],run_time=10,rate_func=linear)
        )

Metadata

Metadata

Assignees

No one assigned

    Labels

    new featureEnhancement specifically adding a new feature (feature request should be used for issues instead)

    Type

    No type

    Projects

    Status

    🆕 New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions