Search code examples
manim

Creating a blinking dot animation with manim


I would like to create an animation with manim, where I have a dot that turns on (instantly become visible) for t_on seconds, and then turns off (instantly become invisible) for t_off seconds. And this procedure repeats for a given total duration, e.g., t_total.

I have written the following code, but it just creates a black animation with no content. Note the values in the below example are t_on=1.5, t_off=0.5, and t_total=6.

from manim import *

class RecursiveDot(Scene):
    def construct(self):
        dot = Dot(color=BLUE, radius=0.2)

        animation = self.create_animation(dot, 6, 1.5, 0.5)

        self.play(animation)

    def create_animation(self, dot, total_duration, visible_duration, invisible_duration):
        num_cycles = int(total_duration // (visible_duration + invisible_duration))
        
        visible_animation = Create(dot, run_time=0)
        invisible_animation = Uncreate(dot, run_time=0)

        animations = []

        for _ in range(num_cycles):
            animations.append(visible_animation)
            animations.append(Wait(invisible_duration))
            animations.append(invisible_animation)
            animations.append(Wait(visible_duration))

        return AnimationGroup(*animations, run_time=total_duration)

And I generate the vide using manim -p -ql dpath_to_where_I_saved_the_module.py RecursiveDot


Solution

  • I think that the use of Create() and Uncreate() is not a good choice, because Create is considered by manim an animation of type "introducer", and Uncreate() of type "remover". The first one will be run only once (even if it appears several times in the AnimationGroup) and the second removes the object from the scene once it ends (the first time).

    I would use instead dot.animate.set_opacity() to change the opacity of the dot between 1 and 0. Since these .animate based transformations are not proper animations, it is neccessary to call .build() after them (.play() does that for you, but if it is part of an AnimationGroup you have to do that manually).

    Also, instead of AnimationGroup() I would use Succession(), that waits until one animation in the list is completed before starting the following one.

    Finally, I think you had reversed your Wait().

    So my proposed code is:

    class Test(Scene):
        def construct(self):
            dot = Dot(color=BLUE, radius=0.2)          
            animation = self.create_animation(dot, 6, 1.5, 0.5)
            self.play(animation)
    
        def create_animation(self, dot, total_duration, visible_duration, invisible_duration):
            num_cycles = int(total_duration // (visible_duration + invisible_duration))
            animations = []
            for _ in range(num_cycles):
                animations.append(dot.animate(run_time=0).set_opacity(1).build())
                animations.append(Wait(visible_duration))
                animations.append(dot.animate(run_time=0).set_opacity(0).build())
                animations.append(Wait(invisible_duration))
            return Succession(*animations, run_time=total_duration)
    

    Which results in:

    Result

    Update

    For debugging purposes, the pass of time can be made visible with the following addition to the scene (before the main animation is called)

            time = 0
            t = always_redraw(lambda :
                    Text(f"Time: {time:.1f}", font_size=24).to_corner(UL)
            )
            def time_tracker(dt):
                nonlocal time
                time += dt
            self.add_updater(time_tracker)
            self.add(t)
    

    This produces

    Result with time