ManimCommunity / ManimCommunity/manim
Draw a line between nodes without going inside
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 40.9k
- Forks
- 3.1k
- Avg merge
- 3d 12h
- Merged PRs (30d)
- 25
Description
## 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:

## Additional comments
We discussed this functionality [here](https://discord.com/channels/581738731934056449/1106238899237421116). 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)
)
```
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing Line, Difference, Rectangle, Arrow, and VMobject behavior referenced in the examples, then compare the Connection prototype across ordinary, boolean-produced, and text objects. Done should provide a supported connection between two objects that stays outside their shapes, works with arrow tips, and updates as either object moves or transforms.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- computer-graphics
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100