diff --git a/example_scenes.py b/example_scenes.py index 9d6a6cee84..0a201e9a44 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -25,18 +25,13 @@ def construct(self): grid = NumberPlane((-10, 10), (-5, 5)) matrix = [[1, 1], [0, 1]] linear_transform_words = VGroup( - Text("This is what the matrix"), - IntegerMatrix(matrix), - Text("looks like") + Text("This is what the matrix"), IntegerMatrix(matrix), Text("looks like") ) linear_transform_words.arrange(RIGHT) linear_transform_words.to_edge(UP) linear_transform_words.set_backstroke(width=5) - self.play( - ShowCreation(grid), - FadeTransform(intro_words, linear_transform_words) - ) + self.play(ShowCreation(grid), FadeTransform(intro_words, linear_transform_words)) self.wait() self.play(grid.animate.apply_matrix(matrix), run_time=3) self.wait() @@ -48,8 +43,8 @@ def construct(self): c_grid.set_stroke(BLUE_E, 1) c_grid.add_coordinate_labels(font_size=24) complex_map_words = TexText(""" - Or thinking of the plane as $\\mathds{C}$,\\\\ - this is the map $z \\rightarrow z^2$ + Or thinking of the plane as $bb(C)$,\\\\ + this is the map $z rightarrow z^2$ """) complex_map_words.to_corner(UR) complex_map_words.set_backstroke(width=5) @@ -70,7 +65,7 @@ def construct(self): class AnimatingMethods(Scene): def construct(self): - grid = Tex(R"\pi").get_grid(10, 10, height=4) + grid = Tex("pi").get_grid(10, 10, height=4) self.add(grid) # You can animate the application of mobject methods with the @@ -101,11 +96,7 @@ def construct(self): # which takes in functions form R^3 to R^3 self.play( grid.animate.apply_function( - lambda p: [ - p[0] + 0.5 * math.sin(p[1]), - p[1] + 0.5 * math.sin(p[0]), - p[2] - ] + lambda p: [p[0] + 0.5 * math.sin(p[1]), p[1] + 0.5 * math.sin(p[0]), p[2]] ), run_time=5, ) @@ -122,9 +113,10 @@ def construct(self): The most important difference between Text and TexText is that\n you can change the font more easily, but can't use the LaTeX grammar """, - font="Arial", font_size=24, + font="Arial", + font_size=24, # t2c is a dict that you can choose color for different text - t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE} + t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE}, ) VGroup(text, difference).arrange(DOWN, buff=1) self.play(Write(text)) @@ -135,7 +127,7 @@ def construct(self): "And you can also set the font according to different words", font="Arial", t2f={"font": "Consolas", "words": "Consolas"}, - t2c={"font": BLUE, "words": GREEN} + t2c={"font": BLUE, "words": GREEN}, ) fonts.set_width(FRAME_WIDTH - 1) slant = Text( @@ -143,7 +135,7 @@ def construct(self): font="Consolas", t2s={"slant": ITALIC}, t2w={"weight": BOLD}, - t2c={"slant": ORANGE, "weight": RED} + t2c={"slant": ORANGE, "weight": RED}, ) VGroup(fonts, slant).arrange(DOWN, buff=0.8) self.play(FadeOut(text), FadeOut(difference, shift=DOWN)) @@ -167,7 +159,7 @@ def construct(self): Tex("A^2 + B^2 = C^2", **kw), Tex("A^2 = C^2 - B^2", **kw), Tex("A^2 = (C + B)(C - B)", **kw), - Tex(R"A = \sqrt{(C + B)(C - B)}", **kw), + Tex(R"A = sqrt((C + B)(C - B))", **kw), ) lines.arrange(DOWN, buff=LARGE_BUFF) @@ -179,7 +171,8 @@ def construct(self): # rearranging an equation self.play( TransformMatchingStrings( - lines[0].copy(), lines[1], + lines[0].copy(), + lines[1], # matched_keys specifies which substring should # line up. If it's not specified, the animation # will align the longest matching substrings. @@ -194,17 +187,12 @@ def construct(self): ), ) self.wait() - self.play(TransformMatchingStrings( - lines[1].copy(), lines[2], - matched_keys=["A^2"] - )) + self.play(TransformMatchingStrings(lines[1].copy(), lines[2], matched_keys=["A^2"])) self.wait() self.play( TransformMatchingStrings( - lines[2].copy(), lines[3], - key_map={"2": R"\sqrt"}, - path_arc=-30 * DEG, - ), + lines[2].copy(), lines[3], key_map={"2": "sqrt"}, path_arc=-30 * DEG + ) ) self.wait(2) self.play(LaggedStartMap(FadeOut, lines, shift=2 * RIGHT)) @@ -228,19 +216,21 @@ def construct(self): class TexIndexing(Scene): def construct(self): # You can index into Tex mobject (or other StringMobjects) by substrings - equation = Tex(R"e^{\pi i} = -1", font_size=144) + equation = Tex("e^(pi i) = -1", font_size=144) self.add(equation) self.play(FlashAround(equation["e"])) self.wait() - self.play(Indicate(equation[R"\pi"])) + self.play(Indicate(equation["pi"])) self.wait() - self.play(TransformFromCopy( - equation[R"e^{\pi i}"].copy().set_opacity(0.5), - equation["-1"], - path_arc=-PI / 2, - run_time=3 - )) + self.play( + TransformFromCopy( + equation["e^(pi i)"].copy().set_opacity(0.5), + equation["-1"], + path_arc=-PI / 2, + run_time=3, + ) + ) self.play(FadeOut(equation)) # Or regular expressions @@ -251,29 +241,29 @@ def construct(self): self.play(FlashAround(part)) self.wait() self.play(FadeOut(equation)) - + # Indexing by substrings like this may not work when # the order in which Latex draws symbols does not match # the order in which they show up in the string. # For example, here the infinity is drawn before the sigma # so we don't get the desired behavior. - equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}", font_size=72) + equation = Tex("sum^infinity_{n = 1} 1 / n^2 = pi^2 / 6", font_size=72) self.play(FadeIn(equation)) - self.play(equation[R"\infty"].animate.set_color(RED)) # Doesn't hit the infinity + self.play(equation["infinity"].animate.set_color(RED)) # Doesn't hit the infinity self.wait() self.play(FadeOut(equation)) # However you can always fix this by explicitly passing in # a string you might want to isolate later. Also, using - # \over instead of \frac helps to avoid the issue for fractions + # frac(num, denom) instead of num / denom helps to avoid the issue for fractions equation = Tex( - R"\sum_{n = 1}^\infty {1 \over n^2} = {\pi^2 \over 6}", + "sum^infinity_(n = 1) frac(1, n^2) = frac(pi^2, 6)", # Explicitly mark "\infty" as a substring you might want to access - isolate=[R"\infty"], - font_size=72 + isolate=["infinity"], + font_size=72, ) self.play(FadeIn(equation)) - self.play(equation[R"\infty"].animate.set_color(RED)) # Got it! + self.play(equation["infinity"].animate.set_color(RED)) # Got it! self.wait() self.play(FadeOut(equation)) @@ -320,10 +310,7 @@ def construct(self): run_time=3, ) self.wait() - self.play( - square.animate.set_width(2), - run_time=3 - ) + self.play(square.animate.set_width(2), run_time=3) self.wait() # In general, you can always call Mobject.add_updater, and pass in @@ -332,9 +319,7 @@ def construct(self): # the mobject and the amount of time since the last frame. now = self.time w0 = square.get_width() - square.add_updater( - lambda m: m.set_width(w0 * math.sin(self.time - now) + w0) - ) + square.add_updater(lambda m: m.set_width(w0 * math.sin(self.time - now) + w0)) self.wait(4 * PI) @@ -360,7 +345,7 @@ def construct(self): # of them, like this. y_axis_config=dict( big_tick_numbers=[-2, 2], - ) + ), ) # Keyword arguments of add_coordinate_labels can be used to # configure the DecimalNumber mobjects which it creates and @@ -375,7 +360,7 @@ def construct(self): # you can call call axes.coords_to_point, abbreviated to # axes.c2p, to associate a set of coordinates with a point, # like so: - dot = Dot(fill_color=RED) + dot = Dot(color=RED) dot.move_to(axes.c2p(0, 0)) self.play(FadeIn(dot, scale=0.5)) self.play(dot.animate.move_to(axes.c2p(3, 2))) @@ -449,7 +434,7 @@ def construct(self): # If it's a string, it treats it as a LaTeX expression. By default # it places the label next to the graph near the right side, and # has it match the color of the graph - sin_label = axes.get_graph_label(sin_graph, "\\sin(x)") + sin_label = axes.get_graph_label(sin_graph, "sin(x)") relu_label = axes.get_graph_label(relu_graph, Text("ReLU")) step_label = axes.get_graph_label(step_graph, Text("Step"), x=4) @@ -471,16 +456,12 @@ def construct(self): parabola = axes.get_graph(lambda x: 0.25 * x**2) parabola.set_stroke(BLUE) - self.play( - FadeOut(step_graph), - FadeOut(step_label), - ShowCreation(parabola) - ) + self.play(FadeOut(step_graph), FadeOut(step_label), ShowCreation(parabola)) self.wait() # You can use axes.input_to_graph_point, abbreviated # to axes.i2gp, to find a particular point on a graph - dot = Dot(fill_color=RED) + dot = Dot(color=RED) dot.move_to(axes.i2gp(2, parabola)) self.play(FadeIn(dot, scale=0.5)) @@ -514,16 +495,18 @@ def construct(self): tex.next_to(axes, UP, buff=0.5) value = tex.make_number_changeable("4.00") - # This will tie the right hand side of our equation to # the square of the radius of the circle - value.add_updater(lambda v: v.set_value(circle.get_radius()**2)) + value.add_updater(lambda v: v.set_value(circle.get_radius() ** 2)) self.add(tex) - text = Text(""" + text = Text( + """ You can manipulate numbers in Tex mobjects - """, font_size=30) + """, + font_size=30, + ) text.next_to(tex, RIGHT, buff=1.5) arrow = Arrow(text, tex) self.add(text, arrow) @@ -539,11 +522,8 @@ def construct(self): # returns a group of the results exponents = tex.make_number_changeable("2", replace_all=True) self.play( - LaggedStartMap( - FlashAround, exponents, - lag_ratio=0.2, buff=0.1, color=RED - ), - exponents.animate.set_color(RED) + LaggedStartMap(FlashAround, exponents, lag_ratio=0.2, buff=0.1, color=RED), + exponents.animate.set_color(RED), ) def func(x, y): @@ -608,17 +588,14 @@ def construct(self): for mob in surfaces[1:]: mob.rotate(PI / 2) - self.play( - Transform(surface, surfaces[1]), - run_time=3 - ) + self.play(Transform(surface, surfaces[1]), run_time=3) self.play( Transform(surface, surfaces[2]), # Move camera frame during the transition self.frame.animate.increment_phi(-10 * DEG), self.frame.animate.increment_theta(-20 * DEG), - run_time=3 + run_time=3, ) # Add ambient rotation self.frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt)) @@ -699,9 +676,14 @@ def setup(self): self.checkbox = Checkbox() self.color_picker = ColorSliders() self.panel = ControlPanel( - Text("Text", font_size=24), self.textbox, Line(), - Text("Show/Hide Text", font_size=24), self.checkbox, Line(), - Text("Color of Text", font_size=24), self.color_picker + Text("Text", font_size=24), + self.textbox, + Line(), + Text("Show/Hide Text", font_size=24), + self.checkbox, + Line(), + Text("Color of Text", font_size=24), + self.color_picker, ) self.add(self.panel) @@ -709,14 +691,14 @@ def construct(self): text = Text("text", font_size=96) def text_updater(old_text): - assert(isinstance(old_text, Text)) + assert isinstance(old_text, Text) new_text = Text(self.textbox.get_value(), font_size=old_text.font_size) # new_text.align_data_and_family(old_text) new_text.move_to(old_text) if self.checkbox.get_value(): new_text.set_fill( color=self.color_picker.get_picked_color(), - opacity=self.color_picker.get_picked_opacity() + opacity=self.color_picker.get_picked_opacity(), ) else: new_text.set_opacity(0) diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index a4a7b9ac6d..76ea6ba465 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -84,6 +84,8 @@ tex: template: "default" # The font size at which Tex("0") has a height of 1 manim unit font_size_for_unit_height: 144 + text_font: "" + math_font: "" text: # font: "Cambria Math" font: "Consolas" diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index c9837db917..dc9a371f36 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -137,10 +137,9 @@ def get_axis_label( edge: Vect3, direction: Vect3, buff: float = MED_SMALL_BUFF, - ensure_on_screen: bool = False, - **kwargs + ensure_on_screen: bool = False ) -> Tex: - label = Tex(label_tex, **kwargs) + label = Tex(label_tex) label.next_to( axis.get_edge_center(edge), direction, buff=buff diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 05945f0edb..161d2ddfe4 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -117,11 +117,7 @@ def element_to_mobject(self, element, **config) -> VMobject: return Tex(str(element), **config) def create_brackets(self, rows, v_buff: float, h_buff: float) -> VGroup: - brackets = Tex("".join(( - R"\left[\begin{array}{c}", - *len(rows) * [R"\quad \\"], - R"\end{array}\right]", - ))) + brackets = Tex(f'vec(delim: "[", {", ".join(["quad"] * len(rows))})') brackets.set_height(rows.get_height() + v_buff) l_bracket = brackets[:len(brackets) // 2] r_bracket = brackets[len(brackets) // 2:] @@ -186,13 +182,13 @@ def swap_entries_for_ellipses( if use_vdots: for column in cols: # Add vdots - dots = Tex(R"\vdots") + dots = Tex("dots.v") dots.set_height(vdots_height) self.swap_entry_for_dots(column[row_index], dots) if use_hdots: for row in rows: # Add hdots - dots = Tex(R"\hdots") + dots = Tex("dots.h") dots.set_width(hdots_width) self.swap_entry_for_dots(row[col_index], dots) if use_vdots and use_hdots: diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 7b427f2db5..65e12b3b93 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -21,11 +21,11 @@ T = TypeVar("T", bound=VMobject) -@lru_cache() +@lru_cache def char_to_cahced_mob(char: str, **text_config): - if "\\" in char or char == "i": + if "$" in char: # This is for when the "character" is a LaTeX command - # like ^\circ or \dots + # like $circle$ or $dots$ return Tex(char, **text_config) else: return Text(char, **text_config) @@ -86,7 +86,7 @@ def set_submobjects_from_number(self, number: float | complex) -> None: # with the intent of calling .copy or .become on them submob_templates = list(map(self.char_to_mob, self.num_string)) if self.show_ellipsis: - dots = self.char_to_mob("...") + dots = self.char_to_mob("$...$") dots.arrange(RIGHT, buff=2 * dots[0].get_width()) submob_templates.append(dots) if self.unit is not None: diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 33e83bf34d..b54d393cad 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -13,6 +13,7 @@ from manimlib.animation.growing import GrowFromCenter from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import TexText +from manimlib.mobject.geometry import Line from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject @@ -35,8 +36,8 @@ def __init__( mobject: Mobject, direction: Vect3 = DOWN, buff: float = 0.2, - tex_string: str = R"\underbrace{\qquad}", - **kwargs + tex_string: str = "underbrace(#h(5em))", + **kwargs, ): super().__init__(tex_string, **kwargs) @@ -55,28 +56,16 @@ def __init__( def set_initial_width(self, width: float): width_diff = width - self.get_width() if width_diff > 0: - for tip, rect, vect in [(self[0], self[1], RIGHT), (self[5], self[4], LEFT)]: - rect.set_width( - width_diff / 2 + rect.get_width(), - about_edge=vect, stretch=True - ) + for tip, rect, vect in [(self[0], self[1], RIGHT), (self[-1], self[-2], LEFT)]: + rect.set_width(width_diff / 2 + rect.get_width(), about_edge=vect, stretch=True) tip.shift(-width_diff / 2 * vect) else: self.set_width(width, stretch=True) return self - def put_at_tip( - self, - mob: Mobject, - use_next_to: bool = True, - **kwargs - ): + def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs): if use_next_to: - mob.next_to( - self.get_tip(), - np.round(self.get_direction()), - **kwargs - ) + mob.next_to(self.get_tip(), np.round(self.get_direction()), **kwargs) else: mob.move_to(self.get_tip()) buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF) @@ -84,13 +73,13 @@ def put_at_tip( mob.shift(self.get_direction() * shift_distance) return self - def get_text(self, text: str, **kwargs) -> Text: + def get_brace_text(self, text: str, **kwargs) -> Text: buff = kwargs.pop("buff", SMALL_BUFF) text_mob = Text(text, **kwargs) self.put_at_tip(text_mob, buff=buff) return text_mob - def get_tex(self, *tex: str, **kwargs) -> Tex: + def get_brace_tex(self, *tex: str, **kwargs) -> Tex: buff = kwargs.pop("buff", SMALL_BUFF) tex_mob = Tex(*tex, **kwargs) self.put_at_tip(tex_mob, buff=buff) @@ -106,6 +95,10 @@ def get_direction(self) -> np.ndarray: vect = self.get_tip() - self.get_center() return vect / get_norm(vect) + def set_symbol_count(self) -> None: + self.symbol_count = [0] * len(self.string) + self.symbol_count[-1] = len(self) + class BraceLabel(VMobject): label_constructor: type = Tex @@ -117,7 +110,7 @@ def __init__( brace_direction: np.ndarray = DOWN, label_scale: float = 1.0, label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF, - **kwargs + **kwargs, ) -> None: super().__init__(**kwargs) self.brace_direction = brace_direction @@ -135,9 +128,7 @@ def __init__( self.set_submobjects([self.brace, self.label]) def creation_anim( - self, - label_anim: Animation = FadeIn, - brace_anim: Animation = GrowFromCenter + self, label_anim: Animation = FadeIn, brace_anim: Animation = GrowFromCenter ) -> AnimationGroup: return AnimationGroup(brace_anim(self.brace), label_anim(self.label)) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index c26a9f19db..5626e83c38 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -78,12 +78,12 @@ class Checkmark(TexTextFromPresetString): - tex: str = R"\ding{51}" + tex: str = "#sym.checkmark" default_color: ManimColor = GREEN class Exmark(TexTextFromPresetString): - tex: str = R"\ding{55}" + tex: str = "#sym.crossmark" default_color: ManimColor = RED diff --git a/manimlib/mobject/svg/special_tex.py b/manimlib/mobject/svg/special_tex.py index 9adb5d7a44..2227e673d0 100644 --- a/manimlib/mobject/svg/special_tex.py +++ b/manimlib/mobject/svg/special_tex.py @@ -22,17 +22,16 @@ def __init__( buff: float = MED_LARGE_BUFF, aligned_edge: Vect3 = LEFT, numbered: bool = False, - **kwargs + **kwargs, ): - labelled_content = [R"\item " + item for item in items] - enum_str = "enumerate" if numbered else "itemize" - tex_string = "\n".join([ - fR"\begin{{{enum_str}}}", - *labelled_content, - fR"\end{{{enum_str}}}" - ]) - tex_text = TexText(tex_string, isolate=labelled_content, **kwargs) - lines = (tex_text.select_part(part) for part in labelled_content) + if numbered: + labelled_content = [f"{num + 1}. " + item for num, item in enumerate(items)] + else: + labelled_content = ["- " + item for item in items] + tex_string = "\n".join([*labelled_content]) + alignment = kwargs.pop("alignment", "left") + tex_text = TexText(tex_string, alignment=alignment, **kwargs) + lines = (tex_text[part] for part in labelled_content) super().__init__(*lines) @@ -51,11 +50,7 @@ class TexTextFromPresetString(TexText): default_color: ManimColor = DEFAULT_MOBJECT_COLOR def __init__(self, **kwargs): - super().__init__( - self.tex, - color=kwargs.pop("color", self.default_color), - **kwargs - ) + super().__init__(self.tex, color=kwargs.pop("color", self.default_color), **kwargs) class Title(TexText): @@ -69,7 +64,7 @@ def __init__( match_underline_width_to_text: bool = False, underline_buff: float = SMALL_BUFF, underline_style: dict = dict(stroke_width=2, stroke_color=GREY_C), - **kwargs + **kwargs, ): super().__init__(*text_parts, font_size=font_size, **kwargs) self.to_edge(UP, buff=MED_SMALL_BUFF) diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index 660a6b32fa..8ea3322072 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -416,10 +416,9 @@ def get_attr_dict_from_command_pair( def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]: return [] - @staticmethod @abstractmethod def get_command_string( - attr_dict: dict[str, str], is_end: bool, label_hex: str | None + self, attr_dict: dict[str, str], is_end: bool, label_hex: str | None ) -> str: return "" diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 9439d348a9..e5b4958392 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,20 +1,19 @@ from __future__ import annotations import re -from pathlib import Path - from functools import lru_cache - from manimlib.config import manim_config +from manimlib.constants import DEFAULT_MOBJECT_COLOR +from manimlib.mobject.geometry import RoundedRectangle from manimlib.mobject.svg.string_mobject import StringMobject from manimlib.mobject.svg.svg_mobject import get_svg_content_height from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.color import color_to_hex -from manimlib.utils.color import hex_to_int -from manimlib.utils.tex_file_writing import latex_to_svg -from manimlib.utils.tex import num_tex_symbols +from manimlib.utils.tex_file_writing import latex2svg from manimlib.logger import log +from manimlib.utils.tex_to_symbol_count import ACCENT_COMMANDS, DELIMITER_COMMANDS +from manimlib.utils.tex_to_symbol_count import OPERATORS, TEX_TO_SYMBOL_COUNT from typing import TYPE_CHECKING @@ -27,26 +26,30 @@ def get_tex_mob_scale_factor() -> float: # Render a reference "0" and calibrate so that font_size_for_unit_height # gives a height of 1 manim unit. Compensates for platform dvisvgm differences. font_size_for_unit_height = manim_config.tex.font_size_for_unit_height - svg_string = latex_to_svg("0", show_message_during_execution=False) + svg_string = latex2svg("0") svg_height = get_svg_content_height(svg_string) return 1.0 / (font_size_for_unit_height * svg_height) class Tex(StringMobject): - tex_environment: str = "align*" + # NOTE: To render fraction, kindly use `frac(a, b)` instead of `a/b` for proper indexing. + tex_environment: str = "$" + font: str = manim_config.tex.math_font def __init__( self, *tex_strings: str, font_size: int = 48, - alignment: str = R"\centering", + alignment: str = "center", + font: str = "", template: str = "", + color: ManimColor = DEFAULT_MOBJECT_COLOR, additional_preamble: str = "", tex_to_color_map: dict = dict(), t2c: dict = dict(), isolate: Selector = [], use_labelled_svg: bool = True, - **kwargs + **kwargs, ): # Combine multi-string arg, but mark them to isolate if len(tex_strings) > 1: @@ -56,30 +59,44 @@ def __init__( tex_string = (" ".join(tex_strings)).strip() - # Prevent from passing an empty string. - if not tex_string.strip(): - tex_string = R"\\" - self.tex_string = tex_string - self.alignment = alignment + self.alignment = f"#set align({alignment})" self.template = template self.additional_preamble = additional_preamble self.tex_to_color_map = dict(**t2c, **tex_to_color_map) + self.font = font or self.font + self.set_font() + super().__init__( tex_string, use_labelled_svg=use_labelled_svg, isolate=isolate, - **kwargs + **kwargs, ) - self.set_color_by_tex_to_color_map(self.tex_to_color_map) self.scale(get_tex_mob_scale_factor() * font_size) - self.font_size = font_size # Important for this to go after the scale call + # horizontal line has no fill. + for mob in self.family_members_with_points(): + if not mob.get_fill_opacity(): + rect = RoundedRectangle(width=mob.get_width(), height=0.025, corner_radius=0.01) + rect.set_fill(mob.get_color(), 1).set_stroke(width=0).move_to(mob) + mob.become(rect) + + self.set_color(color) + self.set_color_by_tex_to_color_map(self.tex_to_color_map) + self.set_symbol_count() + + def set_font(self) -> Self: + self.additional_preamble += f'\n#show math.equation: set text(font: "{self.font}")' + if text_font := manim_config.tex.text_font: + self.additional_preamble += f'\n#set text(font: "{text_font}")' + return self + def get_svg_string_by_content(self, content: str) -> str: - return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string) + return latex2svg(content, self.template, self.additional_preamble) def _handle_scale_side_effects(self, scale_factor: float) -> Self: if hasattr(self, "font_size"): @@ -87,15 +104,17 @@ def _handle_scale_side_effects(self, scale_factor: float) -> Self: return self # Parsing - @staticmethod def get_command_matches(string: str) -> list[re.Match]: # Lump together adjacent brace pairs - pattern = re.compile(r""" + pattern = re.compile( + r""" (?P\\(?:[a-zA-Z]+|.)) - |(?P{+) - |(?P}+) - """, flags=re.X | re.S) + |(?P\(+) + |(?P\)+) + """, + flags=re.X | re.S, + ) result = [] open_stack = [] for match_obj in pattern.finditer(string): @@ -105,15 +124,15 @@ def get_command_matches(string: str) -> list[re.Match]: close_start, close_end = match_obj.span() while True: if not open_stack: - raise ValueError("Missing '{' inserted") + raise ValueError("Missing '(' inserted") (open_start, open_end), index = open_stack.pop() n = min(open_end - open_start, close_end - close_start) - result.insert(index, pattern.fullmatch( - string, pos=open_end - n, endpos=open_end - )) - result.append(pattern.fullmatch( - string, pos=close_start, endpos=close_start + n - )) + result.insert( + index, pattern.fullmatch(string, pos=open_end - n, endpos=open_end) + ) + result.append( + pattern.fullmatch(string, pos=close_start, endpos=close_start + n) + ) close_start += n if close_start < close_end: continue @@ -124,7 +143,7 @@ def get_command_matches(string: str) -> list[re.Match]: else: result.append(match_obj) if open_stack: - raise ValueError("Missing '}' inserted") + raise ValueError("Missing ')' inserted") return result @staticmethod @@ -162,39 +181,33 @@ def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]: @staticmethod def get_color_command(rgb_hex: str) -> str: - rgb = hex_to_int(rgb_hex) - rg, b = divmod(rgb, 256) - r, g = divmod(rg, 256) - return f"\\color[RGB]{{{r}, {g}, {b}}}" + return f'#text(fill: rgb("{rgb_hex}"))' - @staticmethod def get_command_string( - attr_dict: dict[str, str], is_end: bool, label_hex: str | None + self, attr_dict: dict[str, str], is_end: bool, label_hex: str | None ) -> str: if label_hex is None: return "" if is_end: - return "}}" - return "{{" + Tex.get_color_command(label_hex) + return f"{self.tex_environment}]" + return self.get_color_command(label_hex) + f"[{self.tex_environment}" - def get_content_prefix_and_suffix( - self, is_labelled: bool - ) -> tuple[str, str]: + def get_content_prefix_and_suffix(self, is_labelled: bool) -> tuple[str, str]: prefix_lines = [] - suffix_lines = [] + suffix_line = "" + if not is_labelled: - prefix_lines.append(self.get_color_command( - color_to_hex(self.base_color) - )) + prefix_lines.append(self.get_color_command(color_to_hex(self.base_color))) if self.alignment: prefix_lines.append(self.alignment) + + prefix_lines = "".join([line + "\n" for line in prefix_lines]) + if self.tex_environment: - prefix_lines.append(f"\\begin{{{self.tex_environment}}}") - suffix_lines.append(f"\\end{{{self.tex_environment}}}") - return ( - "".join([line + "\n" for line in prefix_lines]), - "".join(["\n" + line for line in suffix_lines]) - ) + prefix_lines += f"{self.tex_environment} " + suffix_line = f" {self.tex_environment}" + + return prefix_lines, suffix_line # Method alias @@ -207,28 +220,115 @@ def get_part_by_tex(self, selector: Selector, index: int = 0) -> VMobject: def set_color_by_tex(self, selector: Selector, color: ManimColor): return self.set_parts_color(selector, color) - def set_color_by_tex_to_color_map( - self, color_map: dict[Selector, ManimColor] - ): + def set_color_by_tex_to_color_map(self, color_map: dict[Selector, ManimColor]): return self.set_parts_color_by_dict(color_map) def get_tex(self) -> str: return self.get_string() + def set_symbol_count(self) -> None: + pattern = rf""" + (?P"[^"]*")| + (?P[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9])| + (?P