From c9b47f0faae9af7e98cd19f615f97156a7474aa7 Mon Sep 17 00:00:00 2001 From: Mindev27 <126376329+Mindev27@users.noreply.github.com> Date: Sat, 18 Jan 2025 13:32:26 +0900 Subject: [PATCH 1/3] Fix code transform animation --- manim/mobject/text/code_transform.py | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 manim/mobject/text/code_transform.py diff --git a/manim/mobject/text/code_transform.py b/manim/mobject/text/code_transform.py new file mode 100644 index 0000000000..22ca4e3991 --- /dev/null +++ b/manim/mobject/text/code_transform.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from manim import AnimationGroup, Code, FadeIn, FadeOut, LaggedStart, Transform, linear + + +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): + """ + An animation that smoothly transitions between two Code objects. + + PARAMETERS + ---------- + before : Code + The initial Code object. + after : Code + The target Code object after the transition. + """ + + 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, + ) From b24e3e43ff4f5e058f4e781ccdeb0de5064dd89f Mon Sep 17 00:00:00 2001 From: Mindev27 <126376329+Mindev27@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:10:59 +0900 Subject: [PATCH 2/3] Remove code_transform.py and integrate transform logic into Code mobject --- manim/mobject/text/code_mobject.py | 144 +++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index eb4e6fed16..a8cc1f1b16 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -15,13 +15,18 @@ from pygments.lexers import get_lexer_by_name, guess_lexer, guess_lexer_for_filename from pygments.styles import get_all_styles +from manim.animation.composition import AnimationGroup, LaggedStart +from manim.animation.fading import FadeIn, FadeOut +from manim.animation.transform import Transform from manim.constants import * from manim.mobject.geometry.arc import Dot from manim.mobject.geometry.shape_matchers import SurroundingRectangle +from manim.mobject.mobject import override_animate from manim.mobject.text.text_mobject import Paragraph from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.typing import StrPath from manim.utils.color import WHITE, ManimColor +from manim.utils.rate_functions import linear class Code(VMobject): @@ -147,6 +152,7 @@ def __init__( raise ValueError("Either a code file or a code string must be specified.") code_string = code_string.expandtabs(tabsize=tab_width) + self._current_code_string = code_string formatter = HtmlFormatter( style=formatter_style, @@ -258,3 +264,141 @@ def get_styles_list(cls) -> list[str]: if cls._styles_list_cache is None: cls._styles_list_cache = list(get_all_styles()) return cls._styles_list_cache + + def _set_current_code_string(self, new_str: str): + self._current_code_string = new_str + + def update_code( + self, + code_file: StrPath | None = None, + code_string: str | None = None, + language: str | None = None, + ) -> Code: + self._target_code_file = code_file + self._target_code_string = code_string + self._target_code_language = language + return self + + @override_animate(update_code) + def _animate_update_code( + self, + code_file: StrPath | None = None, + code_string: str | None = None, + language: str | None = None, + formatter_style: str = "vim", + tab_width: int = 4, + add_line_numbers: bool = True, + line_numbers_from: int = 1, + background: Literal["rectangle", "window"] = "rectangle", + background_config: dict[str, Any] | None = None, + paragraph_config: dict[str, Any] | None = None, + run_time: float | None = None, + rate_func=linear, + lag_ratio: float = 0.0, + **kwargs, + ): + old_code_string = self._current_code_string + old_lines = list(self.code_lines) if hasattr(self, "code_lines") else [] + old_background = getattr(self, "background", None) + old_line_numbers = getattr(self, "line_numbers", None) + + if code_file is not None: + p = Path(code_file) + new_code_str = p.read_text(encoding="utf-8") + else: + new_code_str = code_string + + if not new_code_str: + raise ValueError("No new code_string or code_file found for update_code.") + + if language is None: + language = self._target_code_language + + tmp_new_code = type(self)( + code_file=None, + code_string=new_code_str, + language=language, + formatter_style=formatter_style, + tab_width=tab_width, + add_line_numbers=add_line_numbers, + line_numbers_from=line_numbers_from, + background=background, + background_config=background_config, + paragraph_config=paragraph_config, + ) + new_lines = list(tmp_new_code.code_lines) + new_background = tmp_new_code.background + new_line_numbers = getattr(tmp_new_code, "line_numbers", None) + + matches, deletions, additions = find_line_matches(old_code_string, new_code_str) + + transform_anims = [] + for i, j in matches: + transform_anims.append(Transform(old_lines[i], new_lines[j])) + + fadeout_anims = [] + for i in deletions: + fadeout_anims.append(FadeOut(old_lines[i], remover=True)) + + fadein_anims = [] + for j in additions: + fadein_anims.append(FadeIn(new_lines[j])) + + extra_anims = [] + if old_background and new_background: + extra_anims.append(Transform(old_background, new_background)) + if old_line_numbers and new_line_numbers: + extra_anims.append(Transform(old_line_numbers, new_line_numbers)) + + # if animate codes first, codes covered by background. so background first + all_anims = [] + if extra_anims: + all_anims.append(AnimationGroup(*extra_anims)) + if fadeout_anims: + all_anims.append(AnimationGroup(*fadeout_anims)) + if transform_anims: + all_anims.append(LaggedStart(*transform_anims, lag_ratio=0.0)) + if fadein_anims: + all_anims.append(AnimationGroup(*fadein_anims)) + + final_group = AnimationGroup( + *all_anims, + run_time=run_time, + rate_func=rate_func, + lag_ratio=lag_ratio, + ) + + self.code_lines = tmp_new_code.code_lines + self.line_numbers = new_line_numbers + self.background = tmp_new_code.background + self._set_current_code_string(new_code_str) + + return final_group + + +def find_line_matches(old_code_str: str, new_code_str: str): + """line matching algorithm with bruteforce""" + old_lines = [ + line.lstrip() if line.strip() != "" else None + for line in old_code_str.splitlines() + ] + new_lines = [ + line.lstrip() if line.strip() != "" else None + for line in new_code_str.splitlines() + ] + + matches = [] + for i, o_line in enumerate(old_lines): + if o_line is None: + continue + for j, n_line in enumerate(new_lines): + if n_line is not None and o_line == n_line: + matches.append((i, j)) + old_lines[i] = None + new_lines[j] = None + break + + deletions = [i for i, val in enumerate(old_lines) if val is not None] + additions = [j for j, val in enumerate(new_lines) if val is not None] + + return matches, deletions, additions From 4dabe1eed71101e09a0db6c55d4b3745095bfc58 Mon Sep 17 00:00:00 2001 From: Mindev27 <126376329+Mindev27@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:04:37 +0900 Subject: [PATCH 3/3] Improve type annotations and fix mypy errors in Code mobject --- manim/mobject/text/code_mobject.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index a8cc1f1b16..b103d5fedc 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -7,7 +7,7 @@ ] from pathlib import Path -from typing import Any, Literal +from typing import Any, Callable, Literal from bs4 import BeautifulSoup, Tag from pygments import highlight @@ -108,6 +108,8 @@ def construct(self): directly). """ + line_numbers: Paragraph | None = None + background: VMobject | None = None _styles_list_cache: list[str] | None = None default_background_config: dict[str, Any] = { "buff": 0.3, @@ -265,7 +267,7 @@ def get_styles_list(cls) -> list[str]: cls._styles_list_cache = list(get_all_styles()) return cls._styles_list_cache - def _set_current_code_string(self, new_str: str): + def _set_current_code_string(self, new_str: str) -> None: self._current_code_string = new_str def update_code( @@ -293,10 +295,10 @@ def _animate_update_code( background_config: dict[str, Any] | None = None, paragraph_config: dict[str, Any] | None = None, run_time: float | None = None, - rate_func=linear, + rate_func: Callable[..., float] = linear, lag_ratio: float = 0.0, - **kwargs, - ): + **kwargs: Any, + ) -> AnimationGroup: old_code_string = self._current_code_string old_lines = list(self.code_lines) if hasattr(self, "code_lines") else [] old_background = getattr(self, "background", None) @@ -306,7 +308,7 @@ def _animate_update_code( p = Path(code_file) new_code_str = p.read_text(encoding="utf-8") else: - new_code_str = code_string + new_code_str = "" if code_string is None else code_string if not new_code_str: raise ValueError("No new code_string or code_file found for update_code.") @@ -376,7 +378,9 @@ def _animate_update_code( return final_group -def find_line_matches(old_code_str: str, new_code_str: str): +def find_line_matches( + old_code_str: str, new_code_str: str +) -> tuple[list[tuple[int, int]], list[int], list[int]]: """line matching algorithm with bruteforce""" old_lines = [ line.lstrip() if line.strip() != "" else None