manim/docs/source/guides/using_text.rst
Benjamin Hackl d999d422c9
Introduce first-class support for rendering text and markup via Typst (optional dependency) (#4681)
* feat: add TypstMobject and TypstMathMobject for first-class Typst support

Implement the initial TypstMobject proposal (see agents/typst.md):

- TypstMobject: renders arbitrary Typst markup to SVG via the 'typst'
  Python package (self-contained Rust binary, no system install needed),
  then imports through SVGMobject.
- TypstMathMobject: convenience subclass that wraps input in Typst math
  delimiters ($ ... $).

Pipeline: user string → wrap in minimal Typst document (auto-sized page,
transparent background) → write .typ file → compile to SVG via
typst.compile() → import via SVGMobject → scale/center/recolor.

Key details:
- Compilation helper in manim/utils/typst_file_writing.py with SHA-256
  content-hash caching (same scheme as the LaTeX pipeline).
- font_size property mirrors SingleStringMathTex: compile at fixed 11pt,
  scale after import using initial_height / SCALE_FACTOR_PER_FONT_POINT.
- init_colors() recolors black submobjects to self.color (Typst default
  fill is black), preserving any explicit Typst colors.
- path_string_config enables curve subdivision for smooth animation.
- 'typst' added as optional dependency group in pyproject.toml.
- 10 tests covering creation, font_size, caching, preamble, and repr.

* feat(typst): add sub-expression selection via {{ }} groups and .select()

- Override modify_xml_tree in Typst to convert data-typst-label
  attributes to id attributes before svgelements parsing (avoids
  attribute inheritance issue with data-* attributes)
- Add Typst.select(key) method accepting str labels or int indices
  to retrieve VGroup sub-expressions from id_to_vgroup_dict
- Implement {{ }} double-brace preprocessor on TypstMath:
  - {{ content }} → manimgrp("_grp-N", content) (auto-numbered)
  - {{ content : label }} → manimgrp("label", content) (named)
  - Skips {{ }} inside string literals and [...] content blocks
  - Uses math-mode call convention (no # prefix) to keep args in
    math mode
- Auto-inject manimgrp preamble when groups are detected
- Add 12 new tests covering label mapping, nesting, select(),
  error handling, and preprocessor edge cases

* Remove unneeded assert line

* Generalize some more critical hard-coded LaTeX assumptions

* Fix syntax error

* Support Typst labels in existing APIs

* Add ManimTextLabel typing alias

* Preserve Typst SVG stroke widths by default

* Calibrate Typst font sizing against TeX

* Add Typst mobject documentation

* Track Typst baseline frames

* Document Typst baseline frame tracking

* Add Typst section to text guide

* Polish Typst docs and label handling

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Improve Typst stroke scaling and docs examples

* Tune Typst SVG stroke scaling

* Restore Typst math example wording

* Fix pre-commit issues for Typst branch

* Fix number line runtime typing import

* Exclude Typst autogenerated .rst files

* Use Manim directive in docstrings and fix wrong key in Typst.select() docstring

* Fix GroupedMath comments

---------

Co-authored-by: Toon Verstraelen <Toon.Verstraelen@UGent.be>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <francisco.manriquezn@usm.cl>
2026-06-11 03:54:21 +00:00

572 lines
18 KiB
ReStructuredText

###########################
Rendering Text and Formulas
###########################
There are three different ways by which you can render **Text** in videos:
1. Using Pango (:mod:`~.text_mobject`)
2. Using LaTeX (:mod:`~.tex_mobject`)
3. Using Typst (:mod:`~.typst_mobject`)
Manim's Pango-based text classes include :class:`~.Text`,
:class:`~.MarkupText`, and derivatives such as :class:`~.Paragraph`.
See :ref:`using-text-objects` for more information.
LaTeX rendering is available via :class:`~.Tex` and
:class:`~.MathTex`. See :ref:`rendering-with-latex` for more
information.
Typst support is available via :class:`~.Typst` and
:class:`~.TypstMath`. It offers both general markup and mathematical
typesetting through the Typst compiler without requiring a TeX
distribution. See :ref:`typst-mobjects` for more information.
.. _using-text-objects:
Text Without LaTeX
******************
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 can also
render non-English alphabets like 你好 or こんにちは or 안녕하세요 or
مرحبا بالعالم.
Here is a simple *Hello World* animation.
.. manim:: HelloWorld
:save_last_frame:
:ref_classes: Text
class HelloWorld(Scene):
def construct(self):
text = Text("Hello world", font_size=144)
self.add(text)
You can also use :class:`~.MarkupText` which allows the use of PangoMarkup
(see the documentation of :class:`~.MarkupText` for details) to render text.
For example:
.. manim:: SingleLineColor
:save_last_frame:
:ref_classes: MarkupText
class SingleLineColor(Scene):
def construct(self):
text = MarkupText(
f'all in red <span fgcolor="{YELLOW}">except this</span>', color=RED
)
self.add(text)
.. _Pango library: https://pango.org
Working with :class:`~.Text`
============================
This section explains the properties of :class:`~.Text` and how can it be used
in your animations.
Using Fonts
-----------
You can set a different font using :attr:`~.Text.font`.
.. note::
The font used must be installed in your system, and Pango should know
about it. You can get a list of fonts using :func:`manimpango.list_fonts`.
>>> import manimpango
>>> manimpango.list_fonts()
[...]
.. manim:: FontsExample
:save_last_frame:
class FontsExample(Scene):
def construct(self):
ft = Text("Noto Sans", font="Noto Sans")
self.add(ft)
Setting Slant and Weight
------------------------
Slant is the style of the Text, and it can be ``NORMAL`` (the default),
``ITALIC`` or ``OBLIQUE``. Usually, for many fonts both ``ITALIC`` and
``OBLIQUE`` look similar, but ``ITALIC`` uses **Roman Style**, whereas
``OBLIQUE`` uses **Italic Style**.
Weight specifies the boldness of a font. You can see a list of weights in
:class:`manimpango.Weight`.
.. manim:: SlantsExample
:save_last_frame:
class SlantsExample(Scene):
def construct(self):
a = Text("Italic", slant=ITALIC)
self.add(a)
.. manim:: DifferentWeight
:save_last_frame:
class DifferentWeight(Scene):
def construct(self):
import manimpango
g = VGroup()
weight_list = dict(
sorted(
{
weight: manimpango.Weight(weight).value
for weight in manimpango.Weight
}.items(),
key=lambda x: x[1],
)
)
for weight in weight_list:
g += Text(weight.name, weight=weight.name, font="Open Sans")
self.add(g.arrange(DOWN).scale(0.5))
.. _using-colors:
Using Colors
------------
You can set the color of the text using :attr:`~.Text.color`:
.. manim:: SimpleColor
:save_last_frame:
class SimpleColor(Scene):
def construct(self):
col = Text("RED COLOR", color=RED)
self.add(col)
You can use utilities like :attr:`~.Text.t2c` for coloring specific characters.
This may be problematic if your text contains ligatures
as explained in :ref:`iterating-text`.
:attr:`~Text.t2c` accepts two types of dictionaries,
* The keys can contain indices like ``[2:-1]`` or ``[4:8]``,
this works similar to how `slicing <https://realpython.com/python-strings/#string-slicing>`_
works in Python. The values should be the color of the Text from :class:`~.Color`.
* The keys contain words or characters which should be colored separately
and the values should be the color from :class:`~.Color`:
.. manim:: Textt2cExample
:save_last_frame:
class Textt2cExample(Scene):
def construct(self):
t2cindices = Text('Hello', t2c={'[1:-1]': BLUE}).move_to(LEFT)
t2cwords = Text('World',t2c={'rl':RED}).next_to(t2cindices, RIGHT)
self.add(t2cindices, t2cwords)
If you want to avoid problems when using colors (due to ligatures), consider using
:class:`MarkupText`.
Using Gradients
---------------
You can add a gradient using :attr:`~.Text.gradient`. The value must
be an iterable of any length:
.. manim:: GradientExample
:save_last_frame:
class GradientExample(Scene):
def construct(self):
t = Text("Hello", gradient=(RED, BLUE, GREEN), font_size=96)
self.add(t)
You can also use :attr:`~.Text.t2g` for gradients with specific
characters of the text. It shares a similar syntax to :ref:`the
interface for colors <using-colors>`:
.. manim:: t2gExample
:save_last_frame:
class t2gExample(Scene):
def construct(self):
t2gindices = Text(
'Hello',
t2g={
'[1:-1]': (RED,GREEN),
},
).move_to(LEFT)
t2gwords = Text(
'World',
t2g={
'World':(RED,BLUE),
},
).next_to(t2gindices, RIGHT)
self.add(t2gindices, t2gwords)
Setting Line Spacing
--------------------
You can set the line spacing using :attr:`~.Text.line_spacing`:
.. manim:: LineSpacing
:save_last_frame:
class LineSpacing(Scene):
def construct(self):
a = Text("Hello\nWorld", line_spacing=1)
b = Text("Hello\nWorld", line_spacing=4)
self.add(Group(a,b).arrange(LEFT, buff=5))
.. _disable-ligatures:
Disabling Ligatures
-------------------
By disabling ligatures you would get a one-to-one mapping between characters and
submobjects. This fixes the issues with coloring text.
.. warning::
Be aware that using this method with text that heavily depends on
ligatures (Arabic text) may yield unexpected results.
You can disable ligatures by passing ``disable_ligatures`` to
:class:`Text`. For example:
.. manim:: DisableLigature
:save_last_frame:
class DisableLigature(Scene):
def construct(self):
li = Text("fl ligature",font_size=96)
nli = Text("fl ligature", disable_ligatures=True, font_size=96)
self.add(Group(li, nli).arrange(DOWN, buff=.8))
.. _iterating-text:
Iterating :class:`~.Text`
-------------------------
Text objects behave like :class:`VGroups <.VGroup>`. Therefore, you can slice and index
the text.
For example, you can set each letter to different color by iterating it.
.. manim:: IterateColor
:save_last_frame:
class IterateColor(Scene):
def construct(self):
text = Text("Colors", font_size=96)
for letter in text:
letter.set_color(random_bright_color())
self.add(text)
.. warning::
Please note that `Ligature`_ can cause problems here. If you need a
one-to-one mapping of characters to submobjects you should pass
the ``disable_ligatures`` parameter to :class:`~.Text`.
See :ref:`disable-ligatures`.
.. _Ligature: https://en.wikipedia.org/wiki/Ligature_(writing)
Working with :class:`~.MarkupText`
==================================
MarkupText is similar to :class:`~.Text`, the only difference between them is
that this accepts and processes PangoMarkup (which is similar to
html), instead of just rendering plain text.
Consult the documentation of :class:`~.MarkupText` for more details
and further references about PangoMarkup.
.. manim:: MarkupTest
:save_last_frame:
class MarkupTest(Scene):
def construct(self):
text = MarkupText(
f'<span underline="double" underline_color="green">double green underline</span> in red text<span fgcolor="{YELLOW}"> except this</span>',
color=RED,
font_size=34
)
self.add(text)
.. _rendering-with-typst:
Text With Typst
***************
Manim also supports rendering text and formulas with Typst via
:class:`~.Typst` and :class:`~.TypstMath`.
.. important::
Typst support requires the optional ``typst`` dependency. Install it with
``pip install manim[typst]``.
Typst mobjects compile Typst markup directly to SVG and import the result as
vector graphics. This works both for general markup and for mathematical
expressions.
.. manim:: HelloTypst
:save_last_frame:
:ref_classes: Typst
class HelloTypst(Scene):
def construct(self):
text = Typst(r"*Hello* from _Typst!_", font_size=96)
self.add(text)
For mathematical expressions, use :class:`~.TypstMath`:
.. manim:: HelloTypstMath
:save_last_frame:
:ref_classes: TypstMath
class HelloTypstMath(Scene):
def construct(self):
equation = TypstMath(r"sum_(k=1)^n k = (n(n + 1)) / 2", font_size=72)
self.add(equation)
Typst also supports selecting subexpressions via labels in the Typst source,
or via Manim's ``{{ ... }}`` shorthand in :class:`~.TypstMath`:
.. code-block:: python
eq = TypstMath("{{ a + b : lhs }} = {{ c }}")
eq.select("lhs").set_color(BLUE)
eq.select(0).set_color(YELLOW)
See :ref:`typst-mobjects` for more details and additional examples.
.. _rendering-with-latex:
Text With LaTeX
***************
Just as you can use :class:`~.Text` to add text to your videos, you can
use :class:`~.Tex` to insert LaTeX.
For example,
.. manim:: HelloLaTeX
:save_last_frame:
class HelloLaTeX(Scene):
def construct(self):
tex = Tex(r"\LaTeX", font_size=144)
self.add(tex)
.. note::
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 ``\\`` to escape the backslash: ``Tex('\\LaTeX')``.
Working with :class:`~.MathTex`
===============================
Everything passed to :class:`~.MathTex` is in math mode by default. To be more precise,
:class:`~.MathTex` is processed within an ``align*`` environment. You can achieve a
similar effect with :class:`~.Tex` by enclosing your formula with ``$`` symbols:
``$\xrightarrow{x^6y^8}$``:
.. manim:: MathTeXDemo
:save_last_frame:
class MathTeXDemo(Scene):
def construct(self):
rtarrow0 = MathTex(r"\xrightarrow{x^6y^8}", font_size=96)
rtarrow1 = Tex(r"$\xrightarrow{x^6y^8}$", font_size=96)
self.add(VGroup(rtarrow0, rtarrow1).arrange(DOWN))
LaTeX commands and keyword arguments
====================================
We can use any standard LaTeX commands in the AMS maths packages. Such
as 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', font_size=144)
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, font_size=144)
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 have to 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,
font_size=144,
)
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 by
using :func:`~.set_color_by_tex`, which matches the argument exactly against
the strings passed to the constructor. In this example, we color the
``\bigstar`` part:
.. manim:: LaTeXSubstrings
:save_last_frame:
class LaTeXSubstrings(Scene):
def construct(self):
tex = Tex('Hello', r'$\bigstar$', r'\LaTeX', font_size=144)
tex.set_color_by_tex(r'$\bigstar$', RED)
self.add(tex)
Because :func:`~.set_color_by_tex` requires an exact match, it cannot directly
target a token inside a string that was passed as a single argument. To color
every ``x`` in a formula, use ``substrings_to_isolate`` to split the string at
each occurrence first:
.. manim:: CorrectLaTeXSubstringColoring
:save_last_frame:
class CorrectLaTeXSubstringColoring(Scene):
def construct(self):
equation = MathTex(
r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
substrings_to_isolate="x"
)
equation.set_color_by_tex("x", YELLOW)
self.add(equation)
Each isolated occurrence of ``x`` becomes its own sub-mobject that
:meth:`~.set_color_by_tex` can match exactly.
If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs
to be enclosed by curly brackets.
Note that Manim also supports a custom syntax that allows splitting
a TeX string into substrings easily: simply enclose parts of your formula
that you want to isolate with double braces. In the string
``MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")``, the rendered mobject
will consist of the substrings ``a^2``, ``+``, ``b^2``, ``=``, and ``c^2``.
This makes transformations between similar text fragments easy
to write using :class:`~.TransformMatchingTex`.
For Manim to recognise a ``{{`` as a group opener, it must appear either
at the very start of the string or be immediately preceded by a whitespace
character. This means that ``{{`` embedded directly after non-whitespace
LaTeX — such as ``\frac{{{n}}}{k}`` or ``a^{{2}}`` — is left untouched,
which prevents accidental splitting of ordinary nested-brace expressions.
To stop a leading ``{{`` from being treated as a group opener, insert a
space between the two braces: ``{{ ... }}````{ { ... } }``.
Using ``index_labels`` to work with complicated strings
=======================================================
You might sometimes be working with a very complicated :class:`~.MathTex` mobject
that makes it difficult to work with its individual components. This is
where the debugging function :func:`.index_labels` is very useful.
The method shows the index of a mobject's submobjects, allowing you
to easily find the components of the mobject you would like to change.
.. manim:: IndexLabelsMathTex
:save_last_frame:
class IndexLabelsMathTex(Scene):
def construct(self):
text = MathTex(r"\binom{2n}{n+2}", font_size=96)
# index the first (and only) term of the MathTex mob
self.add(index_labels(text[0]))
text[0][1:3].set_color(YELLOW)
text[0][3:6].set_color(RED)
self.add(text)
LaTeX Maths Fonts - The Template Library
========================================
Changing fonts in LaTeX when typesetting mathematical formulae is
trickier than regular text. It requires changing the template that is used
to compile the TeX. Manim comes with a collection of :class:`~.TexFontTemplates`
ready for you to use. These templates will all work in math 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,
font_size=144,
)
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 script. 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` instead.
.. manim:: LaTeXTemplateLibrary
:save_last_frame:
class LaTeXTemplateLibrary(Scene):
def construct(self):
tex = Tex('Hello 你好 \\LaTeX', tex_template=TexTemplateLibrary.ctex, font_size=144)
self.add(tex)
Aligning formulae
=================
: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', font_size=96)
self.add(tex)