Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code Transform Animation applies to the entire code instead of the changed lines #4103

Open
Mindev27 opened this issue Jan 13, 2025 · 11 comments · May be fixed by #4114
Open

Code Transform Animation applies to the entire code instead of the changed lines #4103

Mindev27 opened this issue Jan 13, 2025 · 11 comments · May be fixed by #4114

Comments

@Mindev27
Copy link

unexpected behavior

When using Manim's Code object to animate code changes, animations are applied to the entire code block, even if only a single line is added.

Video

CodeAnimation.mp4

Expected behavior

The animation should be applied only to the newly added or modified line of code.

Actual behavior

The animation is applied to the entire code block, including unchanged lines, making it unclear which part of the code was modified.

Code:

from manim import *

class CodeAnimation(Scene):
    def construct(self):
        code1 = '''from manim import *

class Animation(Scene):
    def construct(self):
    
        square = Square(side_length=2.0, color=RED)
        
        self.play(Create(square))
        self.wait()
'''

        code2 = '''from manim import *

class Animation(Scene):
    def construct(self):
    
        square = Square(side_length=2.0, color=RED)
        
        square.shift(LEFT * 2)
        
        self.play(Create(square))
        self.wait()
'''

        rendered_code1 = Code(
            code=code1,
            tab_width=4,
            background="window",
            language="Python",
            font="Monospace",
            style="one-dark",
            line_spacing=1
        )
        
        rendered_code2 = Code(
            code=code2,
            tab_width=4,
            background="window",
            language="Python",
            font="Monospace",
            style="one-dark",
            line_spacing=1
        )

        self.play(Write(rendered_code1))
        self.wait()
        
        self.play(Transform(rendered_code1, rendered_code2))
        self.wait()

System specifications

  • Manim version: [v0.18.1]
  • Python version: [3.10.3]
  • Operating system: [Window 11]
@Mindev27
Copy link
Author

I'll create fix PR soon. :)

@marcel-goldschen-ohm
Copy link

I agree, the current code change animations are terrible. I've created a DynamicCode mobject that allows more natural code change animations. Maybe this will be helpful to you. Would be awesome if more natural code diffing animations were the default though.

@Mindev27
Copy link
Author

Hi! I'm really interested in improving the Code Mobject animation.

While looking into it, I realized that the current Code object doesn’t retain text information, so it seems like this would require many internal changes....

This is actually my first contribution, so it feels bit challenging. But I’m excited to keep working on it because I want to create Computer Science videos with Manim in the future.

Do you think it would make sense to build this feature on top of your DynamicCode Mobject?
Any advice or suggestions would be really helpful! 😊

@marcel-goldschen-ohm
Copy link

I'm honestly not sure. There are some things about DynamicCode Mobjects that I don't like (I discuss them in the DynamicCode repo README) which I would rather not have in an optimal solution. If I were you, I'd take whatever useful aspects of DynamicCode you want and see if you could somehow implement them in a new Code object that quacks more like a Manim mobject should.

@Mindev27
Copy link
Author

I checked out your work on DynamicCode and ran it. it works really great :)
It seems quite complex, so I think I need to take more time to fully understand it.

Currently, Manim’s Code object has only SVG, so we should switch it with Text objects. your code do it maybe character level.
I’ll take my time to read through your implementation and try to properly incorporate your add/remove features.

If you have any additional advice or suggestions, I’d really appreciate it. thanks

@marcel-goldschen-ohm
Copy link

Great to hear. To sum up the issues I ran into in developing DynamicCode, a simple Transform from one Code mobject to another looks terrible because it does not understand that unchanged lines of code should remain in place or at most shift around and does not introduce new glyphs as if they were typed. The issue as far as I understand it is that if the SVG vertices between the previous and updated Code objects are not a 1:1 match, the Transform essentially just recreates the new mobject entirely as it can't figure out what you want it to do. To get around this, DynamicCode first moves all unchanged glyphs around in which case there is a 1:1 match with old and new vertices and so Transform shifts these glyphs around in a smooth fashion. After that the new glyphs are added with opacity=0 instantaneously (no animation) to avoid a horrible animation where they spring up from a point or some such, I don't recall exactly. Thereafter the new glyphs are made visible (opacity=1) one by one to simulate typing.

One idea I've had, but haven't looked into to see how feasible it is, is instead of the above hacky process to see if you can specify the transform paths for the glyphs. Might be worth looking into, but not sure it gets you around all of the issues.

Another issue I faced with DynamicCode is that apparently references to mobjects in manim are not guaranteed to remain valid after an animation. I'm relatively new to Manim, so if I'm doing something wrong, someone can please correct me. But I found that the glyph references after animating changes in DynamicCode were stale, and I had to use string and duck tape to go find the new valid glyph references within scene.mobjects (from memory I think that's where I found them) and reset them inside the Code mobject. As far as I'm concerned, this is insane, but apparently that's how Manim works.

All in all, I think the issues popping up here point to larger problems with Manim's design (again, I'm a newbie so I could be wrong), but from reading the docs/discussions I think that at least some if not all of these issues are known to the community but there appears to be little energy for anyone to undertake the gargantuan task of restructuring Manim from the ground up. So maybe hacky solutions like DynamicCode are what is required in the meantime? Or maybe there's a smarter solution you can come up with.

You may also want to have a look at alternative animation libraries such as Motion Canvas which offers nice Code diffing out of the box. In my off the cuff and not well thought out opinion, I think Manim is still nice in comparison to alternatives like Motion Canvas for quick and dirty simple animations. However, if some of the fundamental design flaws are not addressed, I anticipate that things like Motion Canvas will make Manim moot in the not too distant future.

Also, if you use VSCode, you should check out my Manim Viewer extension.

Cheers

@Mindev27
Copy link
Author

thanks for your nice feedback and direction! :)

@Mindev27
Copy link
Author

code

from manim import *

def find_line_matches(before: Code, after: Code):
    before_lines = [
        line.lstrip() if line.strip() != "" else None
        for line in before.code_string.splitlines()
    ]
    after_lines  = [
        line.lstrip() if line.strip() != "" else None
        for line in after.code_string.splitlines()
    ]
    
    matches = []
    
    for i, b_line in enumerate(before_lines):
        if b_line is None:
            continue
        for j, a_line in enumerate(after_lines):
            if a_line is not None and b_line == a_line:
                matches.append((i, j))
                before_lines[i] = None
                after_lines[j] = None
                break
            
    deletions = []
    for i, line in enumerate(before_lines):
        if before_lines[i] is not None:
            deletions.append((i, len(line)))

    additions = []
    for j, line in enumerate(after_lines):
        if after_lines[j] is not None:
            additions.append((j, len(line)))

    return matches, deletions, additions

class CodeTransform(AnimationGroup):
    def __init__(self, before: Code, after: Code, **kwargs):
        matches, deletions, additions = find_line_matches(before, after)

        transform_pairs = [
            (before.code[i], after.code[j])
            for i, j in matches
        ]

        delete_lines = [before.code[i] for i, _ in deletions]

        add_lines = [after.code[j] for j, _ in additions]

        animations = []

        if hasattr(before, 'background_mobject') and hasattr(after, 'background_mobject'):
            animations.append(
                Transform(before.background_mobject, after.background_mobject)
            )

        if hasattr(before, 'line_numbers') and hasattr(after, 'line_numbers'):
            animations.append(
                Transform(before.line_numbers, after.line_numbers)
            )

        if delete_lines:
            animations.append(FadeOut(*delete_lines))

        if transform_pairs:
            animations.append(LaggedStart(*[
                Transform(before_line, after_line)
                for before_line, after_line in transform_pairs
            ]))

        if add_lines:
            animations.append(FadeIn(*add_lines))
            
        super().__init__(
            *animations,
            group=None,
            run_time=None,
            rate_func=linear,
            lag_ratio=0.0,
            **kwargs,
        )


def load_code(code=None):
    code_block = Code(
        code=code,
        tab_width=4,
        background="window",
        language="Python",
        font="Monospace",
        style="one-dark",
        line_spacing=1
    ).scale(0.8)

    return code_block

class Test(Scene):
    def construct(self):
        before_code = '''from manim import *

class Animation(Scene):
    def construct(self):
    
        square = Square(side_length=2.0, color=RED)
        square.shift(LEFT * 2)
        
        self.play(Create(square))
        self.wait()
'''

        after_code = '''from manim import *

class Animation(Scene):
    def construct(self):
    
        circle = Circle(radius=1.0, color=BLUE)
        circle.shift(RIGHT * 2)

        square = Square(side_length=2.0, color=RED)
        square.shift(LEFT * 2)
        
        self.play(Create(circle))
        self.wait()
'''     

        before = load_code(code=before_code)
        after = load_code(code=after_code)

        self.add(before)
        self.wait()

        self.play(CodeTransform(before, after))
        self.wait()

video

Test.mp4

it works good.


I'm currently trying to implement Transform by applying DynamicCode, but it still seems like it will take quite some time.

While searching, I found a pretty decent code snippet. After running it, I noticed that diff-match-patch doesn’t always match beautifully. As an alternative, I naively compared the code line by line to implement move, add, and delete functionalities.

It works better than I expected, and for now, it seems like a good temporary replacement for the terrible animation of the Code object's Transform that I was struggling with. (However, since only completely identical lines are transformed, there are still many issues...)

I’d like to close this issue with the current code. But I really like your DynamicCode, and in the future, I’d love to contribute to modifying the entire Code object using DynamicCode in a new issue. If you could create an issue or mention me in the one you’re working on, I’d be happy to help!

+ Oh, and your Manim Viewer is really useful. Thanks :)

@marcel-goldschen-ohm
Copy link

I wouldn't close this issue as I think this is still a big problem in Manim. For example, does your solution handle inserting/removing characters in the middle of a line?

@behackl behackl reopened this Jan 17, 2025
@Mindev27
Copy link
Author

I agree. Since this is my first open-source contribution, I didn't fully understand the process. Thank you for the feedback.

@marcel-goldschen-ohm
Copy link

marcel-goldschen-ohm commented Jan 17, 2025

Also, nice job finding that code snippet above. Maybe a custom Transform class rather than a custom Mobject is the way to go.

@Mindev27 Mindev27 linked a pull request Jan 18, 2025 that will close this issue
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants