Custom Mobjects#

[1]:
from manim import *
config.media_embed = True; config.media_width = "100%"
_RV = "-v WARNING -qm --progress_bar None --disable_caching Example"
_RI = "-v WARNING -s --progress_bar None --disable_caching Example"
Manim Community v0.15.1

In this course we will create custom VMobjects and VGroups, in the advanced course we will learn how to use the rest of the Mobjects.

The main difference between VMobjects and VGroups is that in VMobjects we cannot use subscripts to manipulate submobjects, while VGroups (and Groups) do accept subscripting.

The recommendation is that if you are going to manipulate the Mobject using subscripts then choose VGroup, if you are not going to manipulate the Mobjectwith subscripts then use VMobject.

Let’s start by explaining how VMobjects are built knowing the corners or control points of the bezier curves.

Koch fractal#

Let’s build a VMobject that returns the Koch fractal, this VMobject needs two points, the starting point \(\vec{p}_s\) and the ending point \(\vec{p}_e\).

The way to get the corners is like this:

b1 b2

  • \(\displaystyle \vec{L}_u:\) Unit vector of \(\vec{L}\).

  • \(\displaystyle \vec{L}_{Ru}:\) \(\vec{L}_u\) rotated 90°.

  • \(\displaystyle \vec{h}=\vec{L}_{Ru} * h\)

  • \(\displaystyle \vec{p}_1 = \vec{p}_s + \frac{\vec{L}}{3}\).

  • \(\displaystyle \vec{p}_2 = \vec{p}_s + \frac{\vec{L}}{2} + \vec{h}\).

  • \(\displaystyle \vec{p}_3 = \vec{p}_s + \frac{2\vec{L}}{3}\).

[2]:
def get_norm(vect):
    return sum((x**2 for x in vect))**0.5
[3]:
class TriangleFractal(VMobject):
    def __init__(self, start, end, **kwargs):
        self.start = start
        self.end = end
        super().__init__(**kwargs)

    def generate_points(self):
        self.set_points_as_corners(
            self.get_next_point(self.start, self.end)
        )

    def get_next_point(self, ps, pe):
        L = pe - ps
        length = get_norm(L)
        L_u = L / length
        L_u_rotated = rotate_vector(L_u, PI/2)
        h = np.sqrt(3) * length / 6
        p1 = ps + L / 3
        p2 = ps + L / 2 + L_u_rotated * h
        p3 = ps + L * 2 / 3
        return [ps, p1, p2, p3, pe]
[4]:
class Example(Scene):
    def construct(self):
        p_s = Dot(LEFT*3)
        p_e = Dot(UR+RIGHT*2)
        l = TriangleFractal(p_s.get_center(), p_e.get_center())

        self.add(p_s, p_e, l)

%manim $_RI
_images/CHP_4_5_0.png

Custom ArrowDashed#

We can also create a VMobject by adding other VMobjects.

Let’s create a custom arrow.

[5]:
class ArrowDashed(VMobject):
    def __init__(self,
                 start,
                 end,
                 reverse=False,
                 buff=0,
                 line_class=DashedLine,
                 tip_angle=20*DEGREES,
                 **kwargs):
        super().__init__(**kwargs)
        if reverse:
            _tmp = start.copy()
            start = end
            end = _tmp
        self.main_line = line_class(start, end, **kwargs)
        self.unit_vector = self.main_line.get_unit_vector()
        self.shift_vector = rotate_vector(self.unit_vector, PI/2)
        self.line_angle = self.main_line.get_angle()
        self.tip_angle = tip_angle

        self.add(self.main_line)
        self.add_right_tip()
        self.add_left_tip()
        self.shift(self.shift_vector * buff)
        self.set_color(self.main_line.get_color())

    def add_right_tip(self, start_angle=0, line_size=0.3, invert=False):
        line = self.main_line
        sign  = -1 if invert else 1
        index = -1 if invert else 0
        tip_vector = line.get_unit_vector() * line_size
        start      = line.get_all_points()[index]
        down_tip   = Line(start , start + tip_vector * sign)
        up_tip     = down_tip.copy()
        normal_line = up_tip.copy()
        down_tip.rotate(self.tip_angle+start_angle, about_point=start)
        up_tip.rotate(-self.tip_angle+start_angle, about_point=start)
        normal_line.rotate(PI/2).move_to(start)
        self.add(down_tip, up_tip, normal_line)

    def add_left_tip(self, start_angle=0, line_size=0.3, invert=True):
        self.add_right_tip(start_angle, line_size, invert)

    def get_label(self, tex, buff=0.2):
        tex.move_to(self.get_center())
        tex.shift(self.shift_vector * buff)
        return tex

    def add_label(self, *args, **kwargs):
        tex = self.get_label(*args, **kwargs)
        self.add(tex)
        return self
[6]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        sq.rotate(30*DEGREES)

        v1, v2 = sq.get_vertices()[:2]
        arrow = ArrowDashed(v1, v2, buff=-0.2, color=RED)
        label = arrow.get_label(MathTex("x"), buff=-0.3)

        self.add(sq)
        self.play(GrowFromCenter(arrow))
        self.play(Write(label))
        self.wait()

%manim $_RV

The creation of VGroups is equivalent to VMobjects, it will be studied in depth in the video tutorials.

Homework#

Make a similar object but with angles, the VMobject must receive an Arc and return another one but with the tips and with a radius greater than the initial arc.

SOLUTION

class ArcDashedVMobject(DashedVMobject):
    def __init__(self,
                 vmobject,
                num_dashes=30,
                dashed_ratio=0.6,
                dash_offset=0.4,
                color=WHITE,
                equal_lengths=True,
                **kwargs):
        super().__init__(vmobject,
                         num_dashes,
                         dashed_ratio,
                         dash_offset,
                         color,
                         equal_lengths,
                         **kwargs)

class ArcArrowDashed(VMobject):
    def __init__(self,
                 arc: Arc,
                 reverse=False,
                 buff=0.2,
                 line_class=ArcDashedVMobject,
                 tip_angle=20*DEGREES,
                 **kwargs):
        super().__init__(**kwargs)
        if reverse:
            _tmp = start.copy()
            start = end
            end = _tmp
        arc_radius = arc.radius + buff
        arc_center = arc.arc_center
        start_angle = arc.start_angle
        angle = arc.angle
        self.arc = Arc(arc_radius,start_angle,angle,arc_center=arc_center,**kwargs)
        if line_class is not None:
            self.main_arc = line_class(self.arc)
        else:
            self.main_arc = self.arc
        self.tip_angle = tip_angle

        self.add(self.main_arc)
        self.add_right_tip()
        self.add_left_tip()
        self.set_color(self.main_arc.get_color())

    def add_right_tip(self, start_angle=0, line_size=0.3, invert=False):
        arc = self.arc
        sign  = -1 if invert else 1
        index = -1 if invert else 0
        tl = TangentLine(arc,abs(index),length=line_size)
        tl.shift(sign * tl.get_vector()/2)
        start    = arc.point_from_proportion(abs(index))
        down_tip = tl
        up_tip   = tl.copy()
        normal_line = tl.copy()
        down_tip.rotate(self.tip_angle+start_angle, about_point=start)
        up_tip.rotate(-self.tip_angle+start_angle, about_point=start)
        normal_line.rotate(PI/2).move_to(start)
        self.add(down_tip, up_tip, normal_line)

    def add_left_tip(self, start_angle=0, line_size=0.3, invert=True):
        self.add_right_tip(start_angle, line_size, invert)

class Example(Scene):
    def construct(self):
        arc = Arc(2.5,-60*DEGREES,PI*0.9)
        measure = ArcArrowDashed(arc,color=RED)
        self.add(arc, measure)
b3