ManimCommunity / manim

A community-maintained Python framework for creating mathematical animations.
https://www.manim.community
MIT License
26.29k stars 1.8k forks source link

Refactoring the Code object/module #3295

Open TimeTravelPenguin opened 1 year ago

TimeTravelPenguin commented 1 year ago

The Code object appears to have an implementation that makes it rather rigid and difficult to work with. It inherits from a class that makes it rather confusing, given that it is in the text namespace, and it seems to have an implementation that breaks several design principles that would help to improve its maintainability and possibility for reuse in other modules and packages.

I would like to propose a refactor or implementation of the module code_mobject from which it derives. I have several suggestions that would serve as a starting point for discussion.

Before I continue, I just want to note that my primary experience in software development lies in other languages where things are done differently from what may be standard in Python. Additionally, I am not entirely familiar with the Manim library at large, so I hope I have not ignored intentional design choices due to some particular reason.

Observed issues

The interface

Problem

This problem is hard to describe and propose a single solution to, so I would like to motivate you with an example.

Suppose I wish to start off with a simple animation between these two code blocks, read from two files. I would like to use something like TransformMatchingTex, but unfortunately, Code is not compatible.

A second example is showing/fading in one line (or word) at a time. I have been told there are "invisible dots", and so steps have to be taken to perform this task. The Code object does make life a lot nicer, providing code highlighting, but there is an additional boilerplate surrounding its use that can likely be reduced.

Proposed solution

I admit that Python is not my strongest, most fluent language, so I do not have the skills to claim my way is a sound solution. So, what I suggest may not be best suited for Python.

I would suggest a form of adaptor interface or some other structural design pattern that best allows the user to interact with Code as if it were a text-like object. I am not sure what would be the best approach, since it makes sense to treat Code as a group. Yet, being that is it a complex structure by nature, not having the powerful tooling available to other text objects makes it difficult to use.

An alternate solution comes from the solution to the following problem.

Too much responsibility

Problem

The Code object has a lot of responsibility. For example, it is able to generate HTML and JSON. The latter of which takes up almost 100 lines of code, or around about 15% of the file. Furthermore, the particular method responsible for generating the JSON is littered with several commented-out print statements, which would indicate a form of debugging that was never removed.

Another detail is that the constructor generates a background from within its own class. Again, by employing the solutions listed in the next section, a user can design their own background and pass the code to it to use.

Proposed Solution

A refactoring of the class, in short. Firstly, putting the Background, HTML, and JSON generators into their own concrete class (with their own interfaces) allows you to implement a behavioural design pattern, such as the strategy or visitor pattern. This additionally allows for other generators to make use of and consume the Code class, were they ever needed to be made.

Secondly, and related to the previously listed problem, a refactor of the class, with the generators taken outside the class's responsibility, the class can be modified to better incorporate other interfaces. As mentioned earlier, if the Code class were to be believed as being able to be animated just as flexibly as other objects, then, since it is complex in nature, another interface may better suit the object. This would provide users with a means to interact with elements that compose the object's structure, which is currently rather difficult to do in complex cases.

As an example, suppose I had a Code object. Perhaps a code.get_word(5) returns a view of the 5th word. Or, if I want a line, code.get_line(3). These needn't necessarily return the internally maintained objects/strings/tex objects. It might make sense to instead return an object that would expose those lower-level components as an animatable object, which allows you to transform and animate the individual components as if they were. I say that since I do not fully understand how everything works beneath the Code object layer. For example, it may be difficult to build a more complex object to provide a better way to animate composed units, such as words, while being able to provide adequate syntax highlighting throughout the animation. Hence, exposing these units (e.g., a word) to be animated or modified (such as for overriding the syntax colour highlighting with something like .set_fill()) would encapsulate the state of that unit, and only expose the necessary tools for animating or modifying that unit.

I want to further describe that last point. Suppose you stored each word individually. Rather than returning a Tex object, or some other such object, you may return something else that allows the object to be consumed by the user. Now, I am not sure how this would be done in Python, so I may be coming across as ignorant, so I do apologise. In a language such as C#, you could return the type by an interface. So, a text-like object may have an IText interface, but you instead return the object as an IAnimatable, which changes how the user can interact with it. In Python, this "idea" may be done functionally, as much of Manim already is, such as by taking functions as arguments.

In closing

The Manim library is inherently a mathematics library and a Code object is something valuable given that there are many areas of STEM that overlap with mathematics, and so the Code class allows folks to incorporate the software side of mathematics into their animations.

I would like to motivate the discussion on extending this part of the Manim library, as it would be very helpful and useful for enhancing the creative possibilities of users by reducing the amount of work and confusing kinks they have to manage themselves.

I would love to have the opportunity to work on this myself, but unfortunately, I am unable to give my attention to this until closer to the end of the year. Assuming this is not addressed, I may very well do so then. For now, I would love for the discussion to begin, in the hopes that there are others out there like me, who would much appreciate using the library to present software animations.

Again, I apologise if I was off with any ideas or suggestions, not having the full understanding I require to properly achieve what I would like to suggest. I have had only a few weeks and limited time to learn the library's source, in the hope of using it for a project, but I ran out of time trying to get the Code object to behave in the way I would like. I did make some good progress, so I do hope my other experiences have allowed me to correctly translate my ideas in a manner that describes good and approachable ideas.

TimeTravelPenguin commented 1 year ago

As a note for either myself in the future, or anyone else, it may be possible to largely reduce the complexity of the class without the need for HTML or JSON generation. I don't know the full specifications of Manim, nor the full complexity regarding LaTeX's use within Manim, but I see that pygments has a LaTeX formatter. I am not sure if the current class has a reason for why it has its current architecture, but it is purely historical, this (or the following) suggestion may better suffice.

While this is one approach, I would propose that a custom formatter may better suite the class, as it would allow for complete control of text using the existing library features. Though, this would require planning as originally mentioned in my original post. I will certainly think on this in a few months, should no one else tackle this, and there is interest in a refactor/reworking. If not, I will happily move this idea into a plugin.

behackl commented 1 year ago

Any improvements to Code are certainly very much welcome, it is one of the classes with the messiest implementation that we currently have.

For any practical purpose, formatting the code via LaTeX is almost surely a step in the wrong direction though. LaTeX is obnoxiously hard to work with when you want to control the structure of the produced output, and I'd strongly advise against doing that. Parsing HTML works well; it is a question of practicality whether it is better to stick with the current approach and just reimplement it, or whether a custom pygments formatter would be easier to maintain in the long run. Someone will have to sit down and try. :-)

TimeTravelPenguin commented 1 year ago

For any practical purpose, formatting the code via LaTeX is almost surely a step in the wrong direction though.

I suspected as much. I didn't like the idea of requiring new packages, so it is more appealing to try an alternative approach.

I'll definitely give it a crack later this year, should no one else pick up interest.

TimeTravelPenguin commented 1 year ago

(Edit: here is a nicer, more up-to-date link of my fork for you to preview. It can be used as a regular MarkupText, unlike my code below)

While my University semester is still set to kick off, I decided to look more into this. I found a new potential implementation that seems to solve every problem, and so I wanted to get the opinions of folks with more experience using Manim.

The Pygments library provides all the tools for lexing and formatting, which is currently part of the architecture. I noticed, however, that Manim has the object MarkupText, which processes PangoMarkup. Incidentally, there is a pygments formatter for that.

In my fork of Manim, where I am currently experimenting, I currently just do

self.Text = highlight(
            self.code_string, lexer, PangoMarkupFormatter(style=self.style)
        )

within the __init__. In the state that the class is currently in (due to experimenting), I can render code really easily

class Test(Scene):
    def construct(self):
        file = "D:/Programming/ForkRepos/manim/testing_code/code.py"
        code_pango = Code(code_file_path=file, style="github-dark").Text  # This is not pretty for now
        code = MarkupText(code_pango).scale(0.5)

        self.play(Create(code))
        self.wait(2)

Result screenshot:

screenshot

The leading alternatives to this would be either to use the HtmlFormatter, as used previously, but with a proper deserializer to then construct a dedicated object encapsulating the total combined structure of the code (e.g. as a VGroup containing all the various parts, as it more or less already is), or a custom formatter -- this latter idea has some kinks in terms of what the formatter interface requires. Both are problematic, either being more complex or by having potentially incorrect use of the formatter's interface (I think, I am not looking into custom formatting until after this semester, if necessary). Hence, this above proposal seems to be a perfect solution.

Are there any limitations regarding the use of MarkupText over other text objects? This approach solves a lot of problems if not. In fact, it would mean that Code would largely be a simple MarkupText with the __init__ logic I have in my fork, and little else (unless it makes sense to add helper functions for animations or something).

MrDiver commented 1 year ago

👍🏼 to me it makes sense that a piece of code is just some MarkupText 🤷🏼 How about the speed of this i remember Code objects being horrificly slow in the past. Does that improve the Situation a little bit?

TimeTravelPenguin commented 1 year ago

How about the speed of this

I don't really remember, to be honest. It did have some issues that I had trouble avoiding such as no progress updating in the console when animating the object. I was working on an alternate solution/implementation, which is where my fork of the repo is currently situated.

For the moment, my implementations are very experimental, but am planning to do some benchmarking (even if it is only basic) when I finish my semester in a few months. Speed, for example, is one important thing I have in mind. It may not improve too much, but it certainly shouldn't get worse.

My current redesign (if my memory serves correctly) inherits from another object and seemed nicer than other experiments. Unfortunately, there were some issues, and I think it comes back to this lovely function. I didn't have time to work out how this works, so the problems with Code may be a bit deeper, and will take some more time, in the future.

MrDiver commented 1 year ago

Okay, that seems good just take your time with it! Happy to have you contributing!

MrDiver commented 1 year ago

3237 Might be fixed by this

TimeTravelPenguin commented 1 year ago

3237 Might be fixed by this

Yeah, I noticed some issues with Code and font ligatures. I was never able to get FiraCode to be recognised. It's odd. I haven't looked into why yet. Definitely something in the back of my mind, though.

MrDiver commented 11 months ago

Any updates ?

TimeTravelPenguin commented 11 months ago

Any updates ?

Apologies for not giving any updates. I am hoping to get into this after New Year's. There are some things up in the air regarding my study, but I should be able to do something either way :)