Skip to content

Starting Animations using Scene.play while others are running #1267

Open
@AntonBallmaier

Description

@AntonBallmaier

First of all: This might just as well be a bug report - I am unsure...

Motivation

Look at the following scene:

WantedScene.mp4

Swiping a line across the Scene and playing a Flash as soon as it hits a dot... Or more generally: Animate one thing and play additional animations based on the state of the animated thing. Sounds like a job for an update function. Consider the following code:

class UsingUpdater(Scene):
    def construct(self):
        #Setup
        dots = VGroup(*[Dot(RIGHT*random.randrange(-6, 6)+UP*random.randrange(-2, 2)) for i in range(10)])
        line = Line(UP*3, DOWN*3).align_on_border(LEFT)
        self.add(dots)
        self.play(Create(line))

        #Updater
        def flash_hit_dots(mob):
            for dot in dots:
                if dot.get_center()[0] < mob.get_center()[0] and not hasattr(dot, "marked"):
                    self.play(Flash(dot, run_time=0.5))
                    dot.marked = True
        line.add_updater(flash_hit_dots)

        #Animate Line
        self.play(line.animate.align_on_border(RIGHT), run_time=5)
        self.wait()

However, this results in ValueError: write to closed file.

Description of proposed feature

The feature request is quite simple, but I think it is hard to implement: Allow using Scene.play() while an Animation is running.

Additional comments

I have seen quite a few different workarounds for this problem, even in 3b1b's code. Some are based on splitting the animation at each new event, others are based on precalculating all timings in advance - but all of them have one thing in common: They are tedious and error-prone. In case you are wondering, how I created the example Scene: Have a look at this mess...

class Workaround(Scene):
    def construct(self):
        #Setup
        dots = VGroup(*[Dot(RIGHT*random.randrange(-6, 6)+UP*random.randrange(-2, 2)) for i in range(10)])
        line = Line(UP*3, DOWN*3).align_on_border(LEFT)
        self.add(dots)
        self.play(Create(line))

        run_time = 5
        rate_func = smooth
        fps = config["frame_rate"]
        
        #First pass to determine frames to start the flash:
        animations = []
        frames = run_time*fps
        x_0 = line.get_center()[0]
        x_1 = line.copy().align_on_border(RIGHT).get_center()[0]
        for t in range(frames):
            alpha = rate_func(t/frames)
            x = x_0 + alpha*(x_1-x_0)
            for dot in dots:
                if dot.get_center()[0] < x and not hasattr(dot, "marked"):
                    #create dummy wait animation:
                    wait = FadeOut(Mobject(), run_time=t/fps)
                    animations.append(AnimationGroup(wait, Flash(dot, run_time=0.5), lag_ratio=1))
                    dot.marked = True
        
        #Run the animations
        self.play(AnimationGroup(
            ApplyMethod(line.align_on_border, RIGHT, run_time=run_time, rate_func=rate_func),
            *animations
        ))
        self.wait()

Metadata

Metadata

Assignees

No one assigned

    Labels

    SuggestionRequesting a feature or change for Manim

    Type

    No type

    Projects

    Status

    📋 Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions