diff --git a/manim/animation/transform.py b/manim/animation/transform.py
index 4f6e11e522..9d694b5c1c 100644
--- a/manim/animation/transform.py
+++ b/manim/animation/transform.py
@@ -30,6 +30,7 @@
import numpy as np
+from .. import config
from ..animation.animation import Animation
from ..constants import DEFAULT_POINTWISE_FUNCTION_RUN_TIME, DEGREES, OUT
from ..mobject.mobject import Group, Mobject
@@ -80,7 +81,10 @@ def begin(self) -> None:
self.target_copy = self.target_mobject.copy()
# Note, this potentially changes the structure
# of both mobject and target_mobject
- self.mobject.align_data(self.target_copy)
+ if config["renderer"] == "opengl":
+ self.mobject.align_data_and_family(self.target_copy)
+ else:
+ self.mobject.align_data(self.target_copy)
super().begin()
def create_target(self) -> typing.Union[Mobject, None]:
diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py
index 021070e1f5..4cff59d5a9 100644
--- a/manim/mobject/mobject.py
+++ b/manim/mobject/mobject.py
@@ -223,7 +223,10 @@ def __deepcopy__(self, clone_from_id):
return result
def __repr__(self):
- return str(self.name)
+ if config["renderer"] == "opengl":
+ return super().__repr__()
+ else:
+ return str(self.name)
def reset_points(self):
"""Sets :attr:`points` to be an empty array."""
diff --git a/manim/mobject/opengl_mobject.py b/manim/mobject/opengl_mobject.py
index 23df0e452c..ba82c6f6ee 100644
--- a/manim/mobject/opengl_mobject.py
+++ b/manim/mobject/opengl_mobject.py
@@ -135,7 +135,101 @@ def animate(self):
# Borrowed from https://github.com/ManimCommunity/manim/
return _AnimationBuilder(self)
+ @property
+ def width(self):
+ """The width of the mobject.
+
+ Returns
+ -------
+ :class:`float`
+
+ Examples
+ --------
+ .. manim:: WidthExample
+
+ class WidthExample(Scene):
+ def construct(self):
+ decimal = DecimalNumber().to_edge(UP)
+ rect = Rectangle(color=BLUE)
+ rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5)
+
+ decimal.add_updater(lambda d: d.set_value(rect.width))
+
+ self.add(rect_copy, rect, decimal)
+ self.play(rect.animate.set(width=7))
+ self.wait()
+
+ See also
+ --------
+ :meth:`length_over_dim`
+
+ """
+
+ # Get the length across the X dimension
+ return self.length_over_dim(0)
+
# Only these methods should directly affect points
+ @width.setter
+ def width(self, value):
+ self.rescale_to_fit(value, 0, stretch=False)
+
+ @property
+ def height(self):
+ """The height of the mobject.
+
+ Returns
+ -------
+ :class:`float`
+
+ Examples
+ --------
+ .. manim:: HeightExample
+
+ class HeightExample(Scene):
+ def construct(self):
+ decimal = DecimalNumber().to_edge(UP)
+ rect = Rectangle(color=BLUE)
+ rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5)
+
+ decimal.add_updater(lambda d: d.set_value(rect.height))
+
+ self.add(rect_copy, rect, decimal)
+ self.play(rect.animate.set(height=5))
+ self.wait()
+
+ See also
+ --------
+ :meth:`length_over_dim`
+
+ """
+
+ # Get the length across the Y dimension
+ return self.length_over_dim(1)
+
+ @height.setter
+ def height(self, value):
+ self.rescale_to_fit(value, 1, stretch=False)
+
+ @property
+ def depth(self):
+ """The depth of the mobject.
+
+ Returns
+ -------
+ :class:`float`
+
+ See also
+ --------
+ :meth:`length_over_dim`
+
+ """
+
+ # Get the length across the Z dimension
+ return self.length_over_dim(2)
+
+ @depth.setter
+ def depth(self, value):
+ self.rescale_to_fit(value, 2, stretch=False)
def resize_points(self, new_length, resize_func=resize_array):
if new_length != len(self.data["points"]):
@@ -619,7 +713,6 @@ def rotate(
self,
angle,
axis=OUT,
- about_point: Union[np.ndarray, List, None] = None,
**kwargs,
):
rot_matrix_T = rotation_matrix_transpose(angle, axis)
diff --git a/manim/mobject/svg/opengl_svg_mobject.py b/manim/mobject/svg/opengl_svg_mobject.py
new file mode 100644
index 0000000000..7e7d0c9442
--- /dev/null
+++ b/manim/mobject/svg/opengl_svg_mobject.py
@@ -0,0 +1,42 @@
+from ...constants import *
+from ..svg.svg_mobject import SVGMobject
+from ..types.opengl_vectorized_mobject import OpenGLVMobject
+from .opengl_svg_path import OpenGLSVGPathMobject
+from .style_utils import cascade_element_style, parse_style
+
+
+class OpenGLSVGMobject(OpenGLVMobject, SVGMobject):
+ def __init__(
+ self,
+ file_name=None,
+ should_center=True,
+ height=2,
+ width=None,
+ unpack_groups=True, # if False, creates a hierarchy of VGroups
+ stroke_width=DEFAULT_STROKE_WIDTH,
+ fill_opacity=1.0,
+ should_subdivide_sharp_curves=False,
+ should_remove_null_curves=False,
+ **kwargs,
+ ):
+ self.def_map = {}
+ self.file_name = file_name or self.file_name
+ self.ensure_valid_file()
+ self.should_center = should_center
+ self.unpack_groups = unpack_groups
+ self.path_string_config = {
+ "should_subdivide_sharp_curves": should_subdivide_sharp_curves,
+ "should_remove_null_curves": should_remove_null_curves,
+ }
+ OpenGLVMobject.__init__(
+ self, stroke_width=stroke_width, fill_opacity=fill_opacity, **kwargs
+ )
+ self.move_into_position(width, height)
+
+ def init_points(self):
+ self.generate_points()
+
+ def path_string_to_mobject(self, path_string: str, style: dict):
+ return OpenGLSVGPathMobject(
+ path_string, **self.path_string_config, **parse_style(style)
+ )
diff --git a/manim/mobject/svg/opengl_svg_path.py b/manim/mobject/svg/opengl_svg_path.py
new file mode 100644
index 0000000000..cd6cf530b8
--- /dev/null
+++ b/manim/mobject/svg/opengl_svg_path.py
@@ -0,0 +1,29 @@
+import numpy as np
+
+from ..types.opengl_vectorized_mobject import OpenGLVMobject
+from .svg_path import SVGPathMobject
+
+
+class OpenGLSVGPathMobject(OpenGLVMobject, SVGPathMobject):
+ def __init__(
+ self,
+ path_string,
+ should_subdivide_sharp_curves=False,
+ should_remove_null_curves=False,
+ **kwargs
+ ):
+ self.path_string = path_string
+ OpenGLVMobject.__init__(
+ self,
+ long_lines=True,
+ should_subdivide_sharp_curves=should_subdivide_sharp_curves,
+ should_remove_null_curves=should_remove_null_curves,
+ **kwargs
+ )
+ self.current_path_start = np.zeros((1, self.dim))
+
+ def init_points(self):
+ self.generate_points()
+
+ def start_new_path(self, point):
+ SVGPathMobject.start_new_path(self, point)
diff --git a/manim/mobject/svg/opengl_tex_mobject.py b/manim/mobject/svg/opengl_tex_mobject.py
new file mode 100644
index 0000000000..1556ebca4e
--- /dev/null
+++ b/manim/mobject/svg/opengl_tex_mobject.py
@@ -0,0 +1,615 @@
+r"""Mobjects representing text rendered using LaTeX.
+
+
+The Tex mobject
++++++++++++++++
+Just as you can use :class:`~.Text` to add text to your videos, you can use :class:`~.Tex` to insert LaTeX.
+
+.. manim:: HelloLaTeX
+ :save_last_frame:
+
+ class HelloLaTeX(Scene):
+ def construct(self):
+ tex = Tex(r'\LaTeX').scale(3)
+ self.add(tex)
+
+Note that we are using a raw string (``r'---'``) instead of a regular string (``'---'``).
+This is because TeX code uses a lot of special characters - like ``\`` for example -
+that have special meaning within a regular python string. An alternative would have
+been to write ``\\`` as in ``Tex('\\LaTeX')``.
+
+The MathTex mobject
++++++++++++++++++++
+Anything enclosed in ``$`` signs is interpreted as maths-mode:
+
+.. manim:: HelloTex
+ :save_last_frame:
+
+ class HelloTex(Scene):
+ def construct(self):
+ tex = Tex(r'$\xrightarrow{x^2y^3}$ \LaTeX').scale(3)
+ self.add(tex)
+
+Whereas in a :class:`~.MathTex` mobject everything is math-mode by default.
+
+.. manim:: MovingBraces
+
+ class MovingBraces(Scene):
+ def construct(self):
+ text=MathTex(
+ "\\frac{d}{dx}f(x)g(x)=", #0
+ "f(x)\\frac{d}{dx}g(x)", #1
+ "+", #2
+ "g(x)\\frac{d}{dx}f(x)" #3
+ )
+ self.play(Write(text))
+ brace1 = Brace(text[1], UP, buff=SMALL_BUFF)
+ brace2 = Brace(text[3], UP, buff=SMALL_BUFF)
+ t1 = brace1.get_text("$g'f$")
+ t2 = brace2.get_text("$f'g$")
+ self.play(
+ GrowFromCenter(brace1),
+ FadeIn(t1),
+ )
+ self.wait()
+ self.play(
+ ReplacementTransform(brace1,brace2),
+ ReplacementTransform(t1,t2)
+ )
+ self.wait()
+
+
+LaTeX commands and keyword arguments
+++++++++++++++++++++++++++++++++++++
+We can use any standard LaTeX commands in the AMS maths packages. For example the ``mathtt`` math-text type, or the ``looparrowright`` arrow.
+
+.. manim:: AMSLaTeX
+ :save_last_frame:
+
+ class AMSLaTeX(Scene):
+ def construct(self):
+ tex = Tex(r'$\mathtt{H} \looparrowright$ \LaTeX').scale(3)
+ self.add(tex)
+
+On the manim side, the :class:`~.Tex` class also accepts attributes to change the appearance of the output.
+This is very similar to the :class:`~.Text` class. For example, the ``color`` keyword changes the color of the TeX mobject:
+
+.. manim:: LaTeXAttributes
+ :save_last_frame:
+
+ class LaTeXAttributes(Scene):
+ def construct(self):
+ tex = Tex(r'Hello \LaTeX', color=BLUE).scale(3)
+ self.add(tex)
+
+Extra LaTeX Packages
+++++++++++++++++++++
+Some commands require special packages to be loaded into the TeX template. For example,
+to use the ``mathscr`` script, we need to add the ``mathrsfs`` package. Since this package isn't loaded
+into manim's tex template by default, we add it manually:
+
+.. manim:: AddPackageLatex
+ :save_last_frame:
+
+ class AddPackageLatex(Scene):
+ def construct(self):
+ myTemplate = TexTemplate()
+ myTemplate.add_to_preamble(r"\usepackage{mathrsfs}")
+ tex = Tex(r'$\mathscr{H} \rightarrow \mathbb{H}$}', tex_template=myTemplate).scale(3)
+ self.add(tex)
+
+Substrings and parts
+++++++++++++++++++++
+The TeX mobject can accept multiple strings as arguments. Afterwards you can refer to the individual
+parts either by their index (like ``tex[1]``), or you can look them up by (parts of) the tex code like
+in this example where we set the color of the ``\bigstar`` using :func:`~.set_color_by_tex`:
+
+.. manim:: LaTeXSubstrings
+ :save_last_frame:
+
+ class LaTeXSubstrings(Scene):
+ def construct(self):
+ tex = Tex('Hello', r'$\bigstar$', r'\LaTeX').scale(3)
+ tex.set_color_by_tex('igsta', RED)
+ self.add(tex)
+
+LaTeX Maths Fonts - The Template Library
+++++++++++++++++++++++++++++++++++++++++
+Changing fonts in LaTeX when typesetting mathematical formulae is a little bit more tricky than
+with regular text. It requires changing the template that is used to compile the tex code.
+Manim comes with a collection of :class:`~.TexFontTemplates` ready for you to use. These templates will all work
+in maths mode:
+
+.. manim:: LaTeXMathFonts
+ :save_last_frame:
+
+ class LaTeXMathFonts(Scene):
+ def construct(self):
+ tex = Tex(r'$x^2 + y^2 = z^2$', tex_template=TexFontTemplates.french_cursive).scale(3)
+ self.add(tex)
+
+Manim also has a :class:`~.TexTemplateLibrary` containing the TeX templates used by 3Blue1Brown. One example
+is the ctex template, used for typesetting Chinese. For this to work, the ctex LaTeX package
+must be installed on your system. Furthermore, if you are only typesetting Text, you probably do not
+need :class:`~.Tex` at all, and should use :class:`~.Text` or :class:`~.PangoText` instead.
+
+.. manim:: LaTeXTemplateLibrary
+ :save_last_frame:
+
+ class LaTeXTemplateLibrary(Scene):
+ def construct(self):
+ tex = Tex('Hello 你好 \\LaTeX', tex_template=TexTemplateLibrary.ctex).scale(3)
+ self.add(tex)
+
+
+Aligning formulae
++++++++++++++++++
+A :class:`~.MathTex` mobject is typeset in the LaTeX ``align*`` environment. This means you can use the ``&`` alignment
+character when typesetting multiline formulae:
+
+.. manim:: LaTeXAlignEnvironment
+ :save_last_frame:
+
+ class LaTeXAlignEnvironment(Scene):
+ def construct(self):
+ tex = MathTex(r'f(x) &= 3 + 2 + 1\\ &= 5 + 1 \\ &= 6').scale(2)
+ self.add(tex)
+"""
+
+__all__ = [
+ "OpenGLTexSymbol",
+ "OpenGLSingleStringMathTex",
+ "OpenGLMathTex",
+ "OpenGLTex",
+ "OpenGLBulletedList",
+ "OpenGLTitle",
+ "OpenGLTexMobject",
+ "OpenGLTextMobject",
+]
+
+
+import operator as op
+from functools import reduce
+
+from ... import config, logger
+from ...constants import *
+from ...mobject.opengl_geometry import OpenGLLine
+from ...mobject.svg.opengl_svg_mobject import OpenGLSVGMobject
+from ...mobject.svg.opengl_svg_path import OpenGLSVGPathMobject
+from ...mobject.types.opengl_vectorized_mobject import (
+ OpenGLVectorizedPoint,
+ OpenGLVGroup,
+)
+from ...utils.color import BLACK
+from ...utils.strings import split_string_list_to_isolate_substrings
+from ...utils.tex_file_writing import tex_to_svg_file
+from .style_utils import parse_style
+
+# from ...utils.tex import TexTemplate
+
+TEX_MOB_SCALE_FACTOR = 0.05
+
+
+class OpenGLTexSymbol(OpenGLSVGPathMobject):
+ """Purely a renaming of SVGPathMobject."""
+
+ pass
+
+
+class OpenGLSingleStringMathTex(OpenGLSVGMobject):
+ """Elementary building block for rendering text with LaTeX.
+
+ Tests
+ -----
+ Check that creating a :class:`~.SingleStringMathTex` object works::
+
+ >>> SingleStringMathTex('Test')
+ SingleStringMathTex('Test')
+ """
+
+ def __init__(
+ self,
+ tex_string,
+ stroke_width=0,
+ fill_opacity=1.0,
+ background_stroke_width=0,
+ background_stroke_color=BLACK,
+ should_center=True,
+ height=None,
+ organize_left_to_right=False,
+ tex_environment="align*",
+ tex_template=None,
+ **kwargs,
+ ):
+ self.organize_left_to_right = organize_left_to_right
+ self.tex_environment = tex_environment
+ if tex_template is None:
+ tex_template = config["tex_template"]
+ self.tex_template = tex_template
+
+ assert isinstance(tex_string, str)
+ self.tex_string = tex_string
+ file_name = tex_to_svg_file(
+ self.get_modified_expression(tex_string),
+ environment=self.tex_environment,
+ tex_template=self.tex_template,
+ )
+ OpenGLSVGMobject.__init__(
+ self,
+ file_name=file_name,
+ should_center=should_center,
+ stroke_width=stroke_width,
+ height=height,
+ fill_opacity=fill_opacity,
+ background_stroke_width=background_stroke_width,
+ background_stroke_color=background_stroke_color,
+ should_subdivide_sharp_curves=True,
+ should_remove_null_curves=True,
+ **kwargs,
+ )
+ if height is None:
+ self.scale(TEX_MOB_SCALE_FACTOR)
+ if self.organize_left_to_right:
+ self.organize_submobjects_left_to_right()
+
+ def __repr__(self):
+ return f"{type(self).__name__}({repr(self.tex_string)})"
+
+ def get_modified_expression(self, tex_string):
+ result = tex_string
+ result = result.strip()
+ result = self.modify_special_strings(result)
+ return result
+
+ def modify_special_strings(self, tex):
+ tex = self.remove_stray_braces(tex)
+ should_add_filler = reduce(
+ op.or_,
+ [
+ # Fraction line needs something to be over
+ tex == "\\over",
+ tex == "\\overline",
+ # Make sure sqrt has overbar
+ tex == "\\sqrt",
+ # Need to add blank subscript or superscript
+ tex.endswith("_"),
+ tex.endswith("^"),
+ tex.endswith("dot"),
+ ],
+ )
+ if should_add_filler:
+ filler = "{\\quad}"
+ tex += filler
+
+ if tex == "\\substack":
+ tex = "\\quad"
+
+ if tex == "":
+ tex = "\\quad"
+
+ # To keep files from starting with a line break
+ if tex.startswith("\\\\"):
+ tex = tex.replace("\\\\", "\\quad\\\\")
+
+ # Handle imbalanced \left and \right
+ num_lefts, num_rights = [
+ len([s for s in tex.split(substr)[1:] if s and s[0] in "(){}[]|.\\"])
+ for substr in ("\\left", "\\right")
+ ]
+ if num_lefts != num_rights:
+ tex = tex.replace("\\left", "\\big")
+ tex = tex.replace("\\right", "\\big")
+
+ for context in ["array"]:
+ begin_in = ("\\begin{%s}" % context) in tex
+ end_in = ("\\end{%s}" % context) in tex
+ if begin_in ^ end_in:
+ # Just turn this into a blank string,
+ # which means caller should leave a
+ # stray \\begin{...} with other symbols
+ tex = ""
+ return tex
+
+ def remove_stray_braces(self, tex):
+ r"""
+ Makes :class:`~.MathTex` resilient to unmatched braces.
+
+ This is important when the braces in the TeX code are spread over
+ multiple arguments as in, e.g., ``MathTex(r"e^{i", r"\tau} = 1")``.
+ """
+
+ # "\{" does not count (it's a brace literal), but "\\{" counts (it's a new line and then brace)
+ num_lefts = tex.count("{") - tex.count("\\{") + tex.count("\\\\{")
+ num_rights = tex.count("}") - tex.count("\\}") + tex.count("\\\\}")
+ while num_rights > num_lefts:
+ tex = "{" + tex
+ num_lefts += 1
+ while num_lefts > num_rights:
+ tex = tex + "}"
+ num_rights += 1
+ return tex
+
+ def get_tex_string(self):
+ return self.tex_string
+
+ def path_string_to_mobject(self, path_string, style):
+ # Overwrite superclass default to use
+ # specialized path_string mobject
+ return OpenGLTexSymbol(
+ path_string, **self.path_string_config, **parse_style(style)
+ )
+
+ def organize_submobjects_left_to_right(self):
+ self.sort(lambda p: p[0])
+ return self
+
+ def init_colors(self, propagate_colors=True):
+ OpenGLSVGMobject.set_style(
+ self,
+ fill_color=self.fill_color or self.color,
+ fill_opacity=self.fill_opacity,
+ stroke_color=self.stroke_color or self.color,
+ stroke_width=self.stroke_width,
+ stroke_opacity=self.stroke_opacity,
+ recurse=propagate_colors,
+ )
+
+
+class OpenGLMathTex(OpenGLSingleStringMathTex):
+ r"""A string compiled with LaTeX in math mode.
+
+ Examples
+ --------
+ .. manim:: Formula
+ :save_last_frame:
+
+ class Formula(Scene):
+ def construct(self):
+ t = MathTex(r"\int_a^b f'(x) dx = f(b)- f(a)")
+ self.add(t)
+
+ Tests
+ -----
+ Check that creating a :class:`~.MathTex` works::
+
+ >>> MathTex('a^2 + b^2 = c^2')
+ MathTex('a^2 + b^2 = c^2')
+
+ """
+
+ def __init__(
+ self,
+ *tex_strings,
+ arg_separator=" ",
+ substrings_to_isolate=None,
+ tex_to_color_map=None,
+ tex_environment="align*",
+ **kwargs,
+ ):
+ self.tex_template = kwargs.pop("tex_template", config["tex_template"])
+ self.arg_separator = arg_separator
+ self.substrings_to_isolate = (
+ [] if substrings_to_isolate is None else substrings_to_isolate
+ )
+ self.tex_to_color_map = tex_to_color_map
+ if self.tex_to_color_map is None:
+ self.tex_to_color_map = {}
+ self.tex_environment = tex_environment
+ tex_strings = self.break_up_tex_strings(tex_strings)
+ self.tex_strings = tex_strings
+ OpenGLSingleStringMathTex.__init__(
+ self,
+ self.arg_separator.join(tex_strings),
+ tex_environment=self.tex_environment,
+ tex_template=self.tex_template,
+ **kwargs,
+ )
+ self.break_up_by_substrings()
+ self.set_color_by_tex_to_color_map(self.tex_to_color_map)
+
+ if self.organize_left_to_right:
+ self.organize_submobjects_left_to_right()
+
+ def break_up_tex_strings(self, tex_strings):
+ substrings_to_isolate = op.add(
+ self.substrings_to_isolate, list(self.tex_to_color_map.keys())
+ )
+ split_list = split_string_list_to_isolate_substrings(
+ tex_strings, *substrings_to_isolate
+ )
+ if self.arg_separator == " ":
+ split_list = [str(x).strip() for x in split_list]
+ # split_list = list(map(str.strip, split_list))
+ split_list = [s for s in split_list if s != ""]
+ return split_list
+
+ def break_up_by_substrings(self):
+ """
+ Reorganize existing submobjects one layer
+ deeper based on the structure of tex_strings (as a list
+ of tex_strings)
+ """
+ new_submobjects = []
+ curr_index = 0
+ for tex_string in self.tex_strings:
+ sub_tex_mob = OpenGLSingleStringMathTex(
+ tex_string,
+ tex_environment=self.tex_environment,
+ tex_template=self.tex_template,
+ )
+ num_submobs = len(sub_tex_mob.submobjects)
+ new_index = curr_index + num_submobs
+ if num_submobs == 0:
+ # For cases like empty tex_strings, we want the corresponding
+ # part of the whole MathTex to be a VectorizedPoint
+ # positioned in the right part of the MathTex
+ sub_tex_mob.submobjects = [OpenGLVectorizedPoint()]
+ sub_tex_mob.assemble_family()
+ last_submob_index = min(curr_index, len(self.submobjects) - 1)
+ sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT)
+ else:
+ sub_tex_mob.submobjects = self.submobjects[curr_index:new_index]
+ sub_tex_mob.assemble_family()
+ new_submobjects.append(sub_tex_mob)
+ curr_index = new_index
+ self.submobjects = new_submobjects
+ self.assemble_family()
+ return self
+
+ def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
+ def test(tex1, tex2):
+ if not case_sensitive:
+ tex1 = tex1.lower()
+ tex2 = tex2.lower()
+ if substring:
+ return tex1 in tex2
+ else:
+ return tex1 == tex2
+
+ return OpenGLVGroup(
+ *[m for m in self.submobjects if test(tex, m.get_tex_string())]
+ )
+
+ def get_part_by_tex(self, tex, **kwargs):
+ all_parts = self.get_parts_by_tex(tex, **kwargs)
+ return all_parts[0] if all_parts else None
+
+ def set_color_by_tex(self, tex, color, **kwargs):
+ parts_to_color = self.get_parts_by_tex(tex, **kwargs)
+ for part in parts_to_color:
+ part.set_color(color)
+ return self
+
+ def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs):
+ for texs, color in list(texs_to_color_map.items()):
+ try:
+ # If the given key behaves like tex_strings
+ texs + ""
+ self.set_color_by_tex(texs, color, **kwargs)
+ except TypeError:
+ # If the given key is a tuple
+ for tex in texs:
+ self.set_color_by_tex(tex, color, **kwargs)
+ return self
+
+ def index_of_part(self, part):
+ split_self = self.split()
+ if part not in split_self:
+ raise ValueError("Trying to get index of part not in MathTex")
+ return split_self.index(part)
+
+ def index_of_part_by_tex(self, tex, **kwargs):
+ part = self.get_part_by_tex(tex, **kwargs)
+ return self.index_of_part(part)
+
+ def sort_alphabetically(self):
+ self.submobjects.sort(key=lambda m: m.get_tex_string())
+
+
+class OpenGLTex(OpenGLMathTex):
+ r"""A string compiled with LaTeX in normal mode.
+
+ Tests
+ -----
+
+ Check whether writing a LaTeX string works::
+
+ >>> Tex('The horse does not eat cucumber salad.')
+ Tex('The horse does not eat cucumber salad.')
+
+ """
+
+ def __init__(
+ self, *tex_strings, arg_separator="", tex_environment="center", **kwargs
+ ):
+ OpenGLMathTex.__init__(
+ self,
+ *tex_strings,
+ arg_separator=arg_separator,
+ tex_environment=tex_environment,
+ **kwargs,
+ )
+
+
+class OpenGLBulletedList(OpenGLTex):
+ def __init__(
+ self,
+ *items,
+ buff=MED_LARGE_BUFF,
+ dot_scale_factor=2,
+ tex_environment=None,
+ **kwargs,
+ ):
+ self.buff = buff
+ self.dot_scale_factor = dot_scale_factor
+ self.tex_environment = tex_environment
+ line_separated_items = [s + "\\\\" for s in items]
+ Tex.__init__(
+ self, *line_separated_items, tex_environment=tex_environment, **kwargs
+ )
+ for part in self:
+ dot = MathTex("\\cdot").scale(self.dot_scale_factor)
+ dot.next_to(part[0], LEFT, SMALL_BUFF)
+ part.add_to_back(dot)
+ self.arrange(DOWN, aligned_edge=LEFT, buff=self.buff)
+
+ def fade_all_but(self, index_or_string, opacity=0.5):
+ arg = index_or_string
+ if isinstance(arg, str):
+ part = self.get_part_by_tex(arg)
+ elif isinstance(arg, int):
+ part = self.submobjects[arg]
+ else:
+ raise TypeError(f"Expected int or string, got {arg}")
+ for other_part in self.submobjects:
+ if other_part is part:
+ other_part.set_fill(opacity=1)
+ else:
+ other_part.set_fill(opacity=opacity)
+
+
+class OpenGLTitle(OpenGLTex):
+ def __init__(
+ self,
+ *text_parts,
+ scale_factor=1,
+ include_underline=True,
+ match_underline_width_to_text=False,
+ underline_buff=MED_SMALL_BUFF,
+ **kwargs,
+ ):
+ self.scale_factor = scale_factor
+ self.include_underline = include_underline
+ self.match_underline_width_to_text = match_underline_width_to_text
+ self.underline_buff = underline_buff
+ Tex.__init__(self, *text_parts, **kwargs)
+ self.scale(self.scale_factor)
+ self.to_edge(UP)
+ if self.include_underline:
+ underline_width = config["frame_width"] - 2
+ underline = OpenGLLine(LEFT, RIGHT)
+ underline.next_to(self, DOWN, buff=self.underline_buff)
+ if self.match_underline_width_to_text:
+ underline.match_width(self)
+ else:
+ underline.width = underline_width
+ self.add(underline)
+ self.underline = underline
+
+
+class OpenGLTexMobject(OpenGLMathTex):
+ def __init__(self, *tex_strings, **kwargs):
+ logger.warning(
+ "TexMobject has been deprecated (due to its confusing name) "
+ "in favour of MathTex. Please use MathTex instead!"
+ )
+ MathTex.__init__(self, *tex_strings, **kwargs)
+
+
+class OpenGLTextMobject(OpenGLTex):
+ def __init__(self, *text_parts, **kwargs):
+ logger.warning(
+ "TextMobject has been deprecated (due to its confusing name) "
+ "in favour of Tex. Please use Tex instead!"
+ )
+ Tex.__init__(self, *text_parts, **kwargs)
diff --git a/manim/mobject/svg/opengl_text_mobject.py b/manim/mobject/svg/opengl_text_mobject.py
new file mode 100644
index 0000000000..1955e09493
--- /dev/null
+++ b/manim/mobject/svg/opengl_text_mobject.py
@@ -0,0 +1,1219 @@
+"""Mobjects used for displaying (non-LaTeX) text.
+
+The simplest way to add text to your animations is to use the :class:`~.Text` class. It uses the Pango library to render text.
+With Pango, you are also able to render non-English alphabets like `你好` or `こんにちは` or `안녕하세요` or `مرحبا بالعالم`.
+
+Examples
+--------
+
+.. manim:: HelloWorld
+ :save_last_frame:
+
+ class HelloWorld(Scene):
+ def construct(self):
+ text = Text('Hello world').scale(3)
+ self.add(text)
+
+.. manim:: TextAlignment
+ :save_last_frame:
+
+ class TextAlignment(Scene):
+ def construct(self):
+ title = Text("K-means clustering and Logistic Regression", color=WHITE)
+ title.scale_in_place(0.75)
+ self.add(title.to_edge(UP))
+
+ t1 = Text("1. Measuring").set_color(WHITE)
+ t1.next_to(ORIGIN, direction=RIGHT, aligned_edge=UP)
+
+ t2 = Text("2. Clustering").set_color(WHITE)
+ t2.next_to(t1, direction=DOWN, aligned_edge=LEFT)
+
+ t3 = Text("3. Regression").set_color(WHITE)
+ t3.next_to(t2, direction=DOWN, aligned_edge=LEFT)
+
+ t4 = Text("4. Prediction").set_color(WHITE)
+ t4.next_to(t3, direction=DOWN, aligned_edge=LEFT)
+
+ x = VGroup(t1, t2, t3, t4).scale_in_place(0.7)
+ x.set_opacity(0.5)
+ x.submobjects[1].set_opacity(1)
+ self.add(x)
+
+"""
+
+# __all__ = ["Text", "Paragraph", "CairoText", "MarkupText", "register_font"]
+
+
+import copy
+import hashlib
+import os
+import re
+import sys
+import typing
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Dict
+from xml.sax.saxutils import escape
+
+import cairo
+import manimpango
+from manimpango import MarkupUtils, PangoUtils, TextSetting
+
+from ... import config, logger
+from ...constants import *
+from ...mobject.opengl_geometry import OpenGLDot
+from ...mobject.svg.opengl_svg_mobject import OpenGLSVGMobject
+from ...mobject.types.opengl_vectorized_mobject import OpenGLVGroup
+from ...utils.color import WHITE, Colors
+
+TEXT_MOB_SCALE_FACTOR = 0.05
+
+
+def remove_invisible_chars(mobject):
+ """Function to remove unwanted invisible characters from some mobject
+
+ Parameters
+ ----------
+ mobject : :class:`~.SVGMobject`
+ Any SVGMobject from which we want to remove unwanted invisible characters.
+
+ Returns
+ -------
+ :class:`~.SVGMobject`
+ The SVGMobject without unwanted invisible characters.
+ """
+
+ iscode = False
+ if mobject.__class__.__name__ == "Text":
+ mobject = mobject[:]
+ elif mobject.__class__.__name__ == "Code":
+ iscode = True
+ code = mobject
+ mobject = mobject.code
+ mobject_without_dots = VGroup()
+ if mobject[0].__class__ == VGroup:
+ for i in range(mobject.__len__()):
+ mobject_without_dots.add(VGroup())
+ mobject_without_dots[i].add(*[k for k in mobject[i] if k.__class__ != Dot])
+ else:
+ mobject_without_dots.add(*[k for k in mobject if k.__class__ != Dot])
+ if iscode:
+ code.code = mobject_without_dots
+ return code
+ return mobject_without_dots
+
+
+class OpenGLParagraph(OpenGLVGroup):
+ r"""Display a paragraph of text.
+
+ For a given :class:`.Paragraph` ``par``, the attribute ``par.chars`` is a
+ :class:`.VGroup` containing all the lines. In this context, every line is
+ constructed as a :class:`.VGroup` of characters contained in the line.
+
+
+ Parameters
+ ----------
+ line_spacing : :class:`int`, optional
+ Represents the spacing between lines. Default to -1, which means auto.
+ alignment : :class:`str`, optional
+ Defines the alignment of paragraph. Default to "left". Possible values are "left", "right", "center"
+
+ Examples
+ --------
+ Normal usage::
+
+ paragraph = Paragraph('this is a awesome', 'paragraph',
+ 'With \nNewlines', '\tWith Tabs',
+ ' With Spaces', 'With Alignments',
+ 'center', 'left', 'right')
+
+ Remove unwanted invisible characters::
+
+ self.play(Transform(remove_invisible_chars(paragraph.chars[0:2]),
+ remove_invisible_chars(paragraph.chars[3][0:3]))
+
+ """
+
+ def __init__(self, *text, line_spacing=-1, alignment=None, **config):
+ self.line_spacing = line_spacing
+ self.alignment = alignment
+ OpenGLVGroup.__init__(self, **config)
+
+ lines_str = "\n".join(list(text))
+ self.lines_text = OpenGLText(lines_str, line_spacing=line_spacing, **config)
+ lines_str_list = lines_str.split("\n")
+ self.chars = self.gen_chars(lines_str_list)
+
+ chars_lines_text_list = OpenGLVGroup()
+ char_index_counter = 0
+ for line_index in range(lines_str_list.__len__()):
+ chars_lines_text_list.add(
+ self.lines_text[
+ char_index_counter : char_index_counter
+ + lines_str_list[line_index].__len__()
+ + 1
+ ]
+ )
+ char_index_counter += lines_str_list[line_index].__len__() + 1
+ self.lines = []
+ self.lines.append([])
+ for line_no in range(chars_lines_text_list.__len__()):
+ self.lines[0].append(chars_lines_text_list[line_no])
+ self.lines_initial_positions = []
+ for line_no in range(self.lines[0].__len__()):
+ self.lines_initial_positions.append(self.lines[0][line_no].get_center())
+ self.lines.append([])
+ self.lines[1].extend(
+ [self.alignment for _ in range(chars_lines_text_list.__len__())]
+ )
+ OpenGLVGroup.__init__(
+ self, *[self.lines[0][i] for i in range(self.lines[0].__len__())], **config
+ )
+ self.move_to(np.array([0, 0, 0]))
+ if self.alignment:
+ self.set_all_lines_alignments(self.alignment)
+
+ def gen_chars(self, lines_str_list):
+ """Function to convert plain string to 2d-VGroup of chars. 2d-VGroup mean "VGroup of VGroup".
+
+ Parameters
+ ----------
+ lines_str_list : :class:`str`
+ Plain text string.
+
+ Returns
+ -------
+ :class:`~.VGroup`
+ The generated 2d-VGroup of chars.
+ """
+ char_index_counter = 0
+ chars = OpenGLVGroup()
+ for line_no in range(lines_str_list.__len__()):
+ chars.add(OpenGLVGroup())
+ chars[line_no].add(
+ *self.lines_text.chars[
+ char_index_counter : char_index_counter
+ + lines_str_list[line_no].__len__()
+ + 1
+ ]
+ )
+ char_index_counter += lines_str_list[line_no].__len__() + 1
+ return chars
+
+ def set_all_lines_alignments(self, alignment):
+ """Function to set all line's alignment to a specific value.
+
+ Parameters
+ ----------
+ alignment : :class:`str`
+ Defines the alignment of paragraph. Possible values are "left", "right", "center".
+ """
+ for line_no in range(0, self.lines[0].__len__()):
+ self.change_alignment_for_a_line(alignment, line_no)
+ return self
+
+ def set_line_alignment(self, alignment, line_no):
+ """Function to set one line's alignment to a specific value.
+
+ Parameters
+ ----------
+ alignment : :class:`str`
+ Defines the alignment of paragraph. Possible values are "left", "right", "center".
+ line_no : :class:`int`
+ Defines the line number for which we want to set given alignment.
+ """
+ self.change_alignment_for_a_line(alignment, line_no)
+ return self
+
+ def set_all_lines_to_initial_positions(self):
+ """Set all lines to their initial positions."""
+ self.lines[1] = [None for _ in range(self.lines[0].__len__())]
+ for line_no in range(0, self.lines[0].__len__()):
+ self[line_no].move_to(
+ self.get_center() + self.lines_initial_positions[line_no]
+ )
+ return self
+
+ def set_line_to_initial_position(self, line_no):
+ """Function to set one line to initial positions.
+
+ Parameters
+ ----------
+ line_no : :class:`int`
+ Defines the line number for which we want to set given alignment.
+ """
+ self.lines[1][line_no] = None
+ self[line_no].move_to(self.get_center() + self.lines_initial_positions[line_no])
+ return self
+
+ def change_alignment_for_a_line(self, alignment, line_no):
+ """Function to change one line's alignment to a specific value.
+
+ Parameters
+ ----------
+ alignment : :class:`str`
+ Defines the alignment of paragraph. Possible values are "left", "right", "center".
+ line_no : :class:`int`
+ Defines the line number for which we want to set given alignment.
+ """
+ self.lines[1][line_no] = alignment
+ if self.lines[1][line_no] == "center":
+ self[line_no].move_to(
+ np.array([self.get_center()[0], self[line_no].get_center()[1], 0])
+ )
+ elif self.lines[1][line_no] == "right":
+ self[line_no].move_to(
+ np.array(
+ [
+ self.get_right()[0] - self[line_no].width / 2,
+ self[line_no].get_center()[1],
+ 0,
+ ]
+ )
+ )
+ elif self.lines[1][line_no] == "left":
+ self[line_no].move_to(
+ np.array(
+ [
+ self.get_left()[0] + self[line_no].width / 2,
+ self[line_no].get_center()[1],
+ 0,
+ ]
+ )
+ )
+
+
+class OpenGLText(OpenGLSVGMobject):
+ r"""Display (non-LaTeX) text rendered using `Pango