ManimCommunity / ManimCommunity/manim

Draw a line between nodes without going inside

Open
#3,238 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

new feature
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:

![image](https://github.com/ManimCommunity/manim/assets/2164118/ba09dcd3-b32b-4e5f-8653-94b5f4be8e3b)

## 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.