Skip to content

Text

The film's code
import manimgx as m


class TextHero(m.Scene):
    def construct(self) -> None:
        title = m.Text("Text", font_size=72)
        styles = m.VGroup(
            m.Text("bold", weight=m.BOLD, font_size=40),
            m.Text("italic", slant=m.ITALIC, font_size=40),
            m.Text("monospace", font="monospace", font_size=40),
            m.Text("red and blue", t2c={"red": m.RED, "blue": m.BLUE}, font_size=40),
        ).arrange(buff=0.6)
        items = m.BulletedList("one idea", "at a time", font_size=40)
        page = m.VGroup(title, styles, items).arrange(m.DOWN, buff=0.7)
        self.play(m.Write(title))
        self.play(m.LaggedStart(*[m.FadeIn(style) for style in styles], lag_ratio=0.2))
        self.play(m.Write(items))
        self.wait()

Text writes words exactly as you give them, in a font: a new line (\n) starts a new line. Its size is font_size, 48 unless you give another. Its parts are its characters, so text[0] is the first one; t2c colors parts of it by their words.

A title, a bulleted list and a paragraph arrange lines of text for you, and a code listing highlights a program's source.

Text

TextExample
Code
import manimgx as m


class TextExample(m.Scene):
    def construct(self) -> None:
        texts = m.VGroup(
            m.Text("New Computer Modern"),
            m.Text("sans-serif: Noto Sans", font="sans-serif"),
            m.Text("monospace: DejaVu Sans Mono", font="monospace"),
            m.Text("Libertinus Serif", font="Libertinus Serif"),
            m.Text("bold and italic", weight=m.BOLD, slant=m.ITALIC),
        )
        self.add(texts.arrange(m.DOWN, buff=0.5).scale(1.3))
TextScriptsExample
Code
import manimgx as m


class TextScriptsExample(m.Scene):
    def construct(self) -> None:
        texts = m.VGroup(
            m.Text("Ελληνικά, кириллица"),
            m.Text("مرحبا بالعالم"),
            m.Text("สวัสดีชาวโลก"),
            m.Text("你好,世界"),
            m.Text("こんにちは、世界"),
            m.Text("안녕하세요, 세계"),
        )
        self.add(texts.arrange(m.DOWN, buff=0.4).scale(1.2))
TextColoringExample
Code
import manimgx as m


class TextColoringExample(m.Scene):
    def construct(self) -> None:
        words = {"red": m.RED, "green": m.GREEN, "blue": m.BLUE}
        word = {"gradient": (m.RED, m.YELLOW)}
        texts = m.VGroup(
            m.Text("red, green and blue", t2c=words),
            m.Text("a slice of the text", t2c={"[2:7]": m.YELLOW}),
            m.Text("a gradient along it all", gradient=(m.BLUE, m.GREEN)),
            m.Text("a gradient in one word", t2g=word),
        )
        self.add(texts.arrange(m.DOWN, buff=0.5).scale(1.3))

Text in a font, typeset by Typst as it is written: white unless styled.

Spaces stay, a newline breaks the line, a tab is tab_width spaces, and nothing is markup ("= Title" is no heading). The lines are set on a grid: their baselines are 1 + line_spacing ems apart, whatever the glyphs on them. The parts are the glyphs, in reading order, spaces excluded: Text("Hello")[0] is the "H", a TypstGlyph. A ligature is one glyph (the "ffi" of "office", in the default font) unless disable_ligatures.

A text is set in one font, from the fonts manimgx ships, so it looks the same on every machine: New Computer Modern unless font names another. "serif", "sans-serif" (or "sans") and "monospace" (or "mono") are New Computer Modern, Noto Sans and DejaVu Sans Mono. Fonts in font_paths come first; a family neither they nor manimgx have is read from the system's fonts, and one found nowhere falls back to New Computer Modern. A character the font lacks is set in another font that has it: manimgx ships Noto faces for Arabic, Hebrew, the Indic scripts, Thai, Chinese, Japanese, Korean and more, with symbols and emoji. In any font, a capital is 0.449 scene units tall at the default size, 48, and in proportion at others: each font is scaled to that, and font_size still reads the size asked for.

A text whose first letter is from a right-to-left script (Arabic, Hebrew) is set as a right-to-left paragraph, its lines lined up on the right. A Chinese, Japanese or Korean text takes its language from its characters (Japanese with kana, Korean with hangul, else Chinese), which gives its Han characters their regional forms.

Keys pick characters: a slice "[a:b]" of the text as written, spaces included, or a string, each occurrence of it (a key that occurs nowhere picks nothing). t2s and t2w set what they pick in another slant or weight; gradient, then t2g, then t2c color it, each over the one before. Color never changes the layout: the text is laid out once, and each glyph takes the color of the characters it draws — a ligature that a color boundary crosses is cut there, each piece in its characters' color. A custom glyph's path-building methods also construct its color pieces.

m.Text(text, line_spacing=-1, font='', slant=NORMAL, weight=NORMAL, t2c=None, t2g=None, t2s=None, t2w=None, gradient=None, tab_width=4, disable_ligatures=False, **kwargs)
text

The text, as written.

line_spacing

The space between lines, in ems: the baselines are 1 + line_spacing ems apart; -1 for 0.3.

font

The font family, or "serif", "sans-serif" or "monospace"; "" for New Computer Modern.

slant

NORMAL, ITALIC or OBLIQUE.

weight

A weight by name, from THIN through NORMAL and BOLD to ULTRAHEAVY; the font's nearest weight is used.

t2c

A color for each key.

t2g

A gradient for each key: its colors spread along what the key picks.

t2s

A slant for each key.

t2w

A weight for each key.

gradient

Colors spread along the whole text, character by character, spaces included.

tab_width

How many spaces a tab is set as.

disable_ligatures

Whether every character is a glyph of its own.

It also takes the Typst keywords and the style keywords.

Source

src/manimgx/mobjects/text.py

@prototype
def __init__(
    self,
    text: str,
    line_spacing: float = -1,
    font: str = "",
    slant: str = NORMAL,
    weight: str = NORMAL,
    t2c: Mapping[str, ParsableManimColor] | None = None,
    t2g: Mapping[str, Sequence[ParsableManimColor]] | None = None,
    t2s: Mapping[str, str] | None = None,
    t2w: Mapping[str, str] | None = None,
    gradient: Sequence[ParsableManimColor] | None = None,
    tab_width: int = 4,
    disable_ligatures: bool = False,
    **kwargs: Unpack[TypstOptions],
) -> None:
    if kwargs.get("fill_opacity") is None and kwargs.get("opacity") is None:
        kwargs["fill_opacity"] = 1.0  # filled, unless an opacity is given
    self.text = text
    """The text, as it was written."""
    self.original_text = text
    self.font = font
    self.slant, self.weight = slant, weight
    chars = text  # the text as written, as characters
    family = font or DEFAULT_FONT
    base: Style = (weight, slant)
    styles: list[Style] | None = None
    if t2w or t2s:
        styles = [base] * len(chars)
        for field, mapping in enumerate((t2w, t2s)):
            for key, value in (mapping or {}).items():
                for span in _find(chars, key):
                    for i in span:
                        style = list(styles[i])
                        style[field] = value
                        styles[i] = (style[0], style[1])
    ligatures = ", ligatures: false" if disable_ligatures else ""
    head = f"#text(font: {_fonts(family)}, {_arguments(base)}{ligatures})["
    body, runs = self._source(text, tab_width, len(head.encode()), base, styles)
    rules = _rules(text) + (_lines(line_spacing) if "\n" in text else "")
    if rules:
        kwargs["typst_preamble"] = rules + kwargs.get("typst_preamble", "")
    # set larger by its font's calibration, so font_size stays the size asked for
    super().__init__(
        f"{head}{body}]", font_scale=_calibration(family, kwargs), **kwargs
    )
    self._chars = chars
    self._clusters = self._characters(runs)
    colors: dict[int, ManimColor] = {}
    if gradient:
        colors |= dict(enumerate(color_gradient(list(gradient), len(chars))))
    for key, stops in (t2g or {}).items():
        for span in _find(chars, key):
            if span:
                colors |= dict(zip(span, color_gradient(list(stops), len(span))))
    for key, color in (t2c or {}).items():
        for span in _find(chars, key):
            colors |= dict.fromkeys(span, ManimColor(color))
    if colors:
        self._paint(colors)

text

The text, as it was written.

Paragraph

ParagraphExample
Code
import manimgx as m


class ParagraphExample(m.Scene):
    def construct(self) -> None:
        lines = ("Twinkle, twinkle,", "little star,", "how I wonder")
        paragraphs = m.VGroup(
            m.Paragraph(*lines),
            m.Paragraph(*lines, alignment="center", line_spacing=1),
            m.Paragraph(*lines, alignment="right"),
        )
        self.add(paragraphs.arrange(buff=0.8).scale(0.85))

Lines of text set as one paragraph: on one grid of baselines, each line at the paragraph's start or where alignment puts it.

The lines are one Text (lines_text), joined by newlines, so they share its grid: their baselines are 1 + line_spacing ems apart (1.3 unless line_spacing is given). The paragraph's parts are its lines, from the top, each the group of its glyphs.

m.Paragraph(*text, alignment=None, **kwargs)
*text

The lines: each string is one, or several, split at its newlines.

alignment

"left", "center" or "right" to line the lines up on that side or center them; None to start each at the paragraph's start (the left, for left-to-right text).

It also takes the Text keywords and the style keywords.

Source

src/manimgx/mobjects/text.py

def __init__(
    self,
    *text: str,
    alignment: Literal["left", "center", "right"] | None = None,
    **kwargs: Unpack[TextOptions],
) -> None:
    joined = "\n".join(text)
    if alignment is not None:  # Typst's alignments of these names
        kwargs["typst_preamble"] = f"#set align({alignment})\n" + kwargs.get(
            "typst_preamble", ""
        )
    self.lines_text = whole = Text(joined, **kwargs)
    """The lines as one [Text][manimgx.Text], joined by newlines: the paragraph's
    lines group its glyphs."""
    # the parts are the lines, each the group of its glyphs (CE's structure)
    starts = [0, *(i + 1 for i, ch in enumerate(joined) if ch == "\n")]
    lines: list[list[Mobject]] = [[] for _ in starts]
    for part, chars in zip(whole.submobjects, whole._clusters):
        lines[bisect.bisect_right(starts, chars.start) - 1].append(part)
    super().__init__(*(VGroup(*line) for line in lines))
    self.alignment = alignment

lines_text

The lines as one Text, joined by newlines: the paragraph's lines group its glyphs.

Title

Code
import manimgx as m


class TitleExample(m.Scene):
    def construct(self) -> None:
        title = m.Title(r"The Pythagorean theorem: $a^2 + b^2 = c^2$")
        self.play(m.Write(title))

A title: LaTeX text at the top of the frame, over a line across it.

The line is the last part, and the title's underline; a color colors it with the text, as set_color would.

m.Title(*text_parts, include_underline=True, match_underline_width_to_text=False, underline_buff=MED_SMALL_BUFF, **kwargs)
*text_parts

The title, in LaTeX: a string per part.

include_underline

Whether a line is drawn under the title.

match_underline_width_to_text

Whether the line is as wide as the title; if not, it spans the frame but 1 unit at each side.

underline_buff

The gap between the title and the line, in scene units.

It also takes the MathTex keywords and the style keywords.

Source

src/manimgx/mobjects/text.py

def __init__(
    self,
    *text_parts: str,
    include_underline: bool = True,
    match_underline_width_to_text: bool = False,
    underline_buff: float = MED_SMALL_BUFF,
    **kwargs: Unpack[MathTexOptions],
) -> None:
    from manimgx.config import config
    from manimgx.constants import DOWN, LEFT, RIGHT, UP
    from manimgx.mobjects.shapes import Line

    super().__init__(*text_parts, **kwargs)
    self.to_edge(UP)
    if include_underline:
        underline = Line(LEFT, RIGHT).next_to(self, DOWN, buff=underline_buff)
        underline.width = (
            self.width if match_underline_width_to_text else config.frame_width - 2
        )
        color = kwargs.get("color")
        if color is not None:  # color= colors the whole title, as set_color does
            underline.set_color(color)
        self.add(underline)
        self.underline = underline
        """The line under the title, its last part (a title made without one has
        none)."""

underline

The line under the title, its last part (a title made without one has none).

BulletedList

Code
import manimgx as m


class BulletedListExample(m.Scene):
    def construct(self) -> None:
        items = m.BulletedList(
            "Typeset in the engine",
            "No LaTeX installation",
            r"Math too: $a^2 + b^2 = c^2$",
            font_size=72,
        )
        self.add(items)
        self.play(items.animate.fade_all_but(1))

A bulleted list: an item per line, each after a bullet, lined up on the left.

Typst sets the items as a list: each bullet is a centered dot (LaTeX's \cdot at twice its size), SMALL_BUFF from its item, and the items are buff apart, in scene units at the list's font size. Each item is LaTeX text (math between $ signs) and a part of its own, its bullet first: items[1] is the second item. height and width scale the list as set.

m.BulletedList(*items, buff=MED_LARGE_BUFF, **kwargs)
*items

The items, in LaTeX.

buff

The space between items, in scene units.

It also takes the MathTex keywords and the style keywords.

Source

src/manimgx/mobjects/text.py

def __init__(
    self,
    *items: str,
    buff: float = MED_LARGE_BUFF,
    **kwargs: Unpack[MathTexOptions],
) -> None:
    self.buff = buff
    pt = 1 / (
        kwargs.get("font_size", DEFAULT_FONT_SIZE) * SCALE_FACTOR_PER_FONT_POINT
    )
    kwargs["typst_preamble"] = (
        f"#set list(marker: [#box(scale(200%, $dot.c$))<{_BULLET}>], indent: 0pt,"
        f" body-indent: {SMALL_BUFF * pt}pt, spacing: {buff * pt}pt)\n"
        + kwargs.get("typst_preamble", "")
    )
    kwargs.setdefault("tex_environment", None)
    super().__init__(*items, **kwargs)

fade_all_but

Fade every item but one: the others' fill to opacity, the one's to 1.

bulleted_list.fade_all_but(index, opacity=0.5)
index

The item's index, from 0.

opacity

The other items' fill opacity, from 0 to 1.

Source

src/manimgx/mobjects/text.py

def fade_all_but(self, index: int, opacity: float = 0.5) -> Self:
    """Fade every item but one: the others' fill to `opacity`, the one's to 1.

    Args:
        index: The item's index, from 0.
        opacity: The other items' fill opacity, from 0 to 1.
    """
    part = self.submobjects[index]
    for other in self.submobjects:
        other.set_fill(opacity=1 if other is part else opacity)
    return self

Code

CodeExample
Code
import manimgx as m


class CodeExample(m.Scene):
    def construct(self) -> None:
        python = m.Code(
            code_string='def greet(name):\n    return f"Hello, {name}!"\n',
            language="python",
        )
        rust = m.Code(
            code_string='fn main() {\n    println!("Hello!");\n}\n',
            language="rust",
            background="window",
            add_line_numbers=False,
        )
        self.add(m.VGroup(python, rust).arrange(m.DOWN, buff=0.5).scale(1.4))

A listing of source code, highlighted: its lines, their numbers, and a rectangle or a window behind them.

The code is highlighted by Typst's grammars, in the colors of formatter_style, and set on a fixed grid, each character a column. The parts are background, line_numbers (if the lines are numbered) and code_lines, whose i-th part is the i-th line, a group of its glyphs, spaces excluded. Tabs are expanded, and blank lines at the start and at the end are dropped.

m.Code(code_file=None, code_string=None, language=None, formatter_style='vim', tab_width=4, add_line_numbers=True, line_numbers_from=1, background='rectangle', background_config=None, paragraph_config=None)
code_file

A file to read the code from (UTF-8); its extension names the language unless language does.

code_string

The code, when there is no code_file; one of the two is needed (ValueError otherwise).

language

The language to highlight the code as, by name or file extension ("python", "rust", "cpp", "js", …: those Typst's raw knows); None, or a language it doesn't know, for plain text.

formatter_style

The colors: "vim", the only style, on a black background.

tab_width

How many columns apart the tab stops are.

add_line_numbers

Whether the lines are numbered, on their left.

line_numbers_from

The first line's number.

background

"rectangle", or "window": a rectangle with a window's three buttons at its top.

background_config

Surrounding rectangle keywords for the background, over default_background_config: a margin of 0.3, round corners, a thin white outline, and the style's background color.

paragraph_config

Code text keywords for the text, over default_paragraph_config.

Source

src/manimgx/mobjects/code.py

def __init__(
    self,
    code_file: StrPath | None = None,
    code_string: str | None = None,
    language: str | None = None,
    formatter_style: CodeStyle = "vim",
    tab_width: int = 4,
    add_line_numbers: bool = True,
    line_numbers_from: int = 1,
    background: Literal["rectangle", "window"] = "rectangle",
    background_config: FrameOptions | None = None,
    paragraph_config: CodeText | None = None,
) -> None:
    super().__init__()
    if code_file is not None:
        code_string = Path(code_file).read_text(encoding="utf-8")
        language = language or Path(code_file).suffix.removeprefix(".") or None
    elif code_string is None:
        raise ValueError("Either a code file or a code string must be specified.")
    style = _STYLES[formatter_style]
    text = self.default_paragraph_config | (paragraph_config or {})
    # the lines as pygments hands them to CE: tabs expanded, blank lines trimmed off
    code = (
        code_string.expandtabs(tab_width).replace("\r\n", "\n").replace("\r", "\n")
    )
    lines = code.strip("\n").split("\n")
    if not any(line.strip() for line in lines):
        lines = [""] * len(lines)

    # CE closes the first and last lines with an ascender, a descender and the first line
    # number, so that a listing's height depends on its lines alone, then takes them away
    suffix = f" pA{line_numbers_from}"
    closing = len(suffix) - 1  # its glyphs: all but the space
    rows = _typeset(
        lines, text, style.foreground, style=style, language=language, suffix=suffix
    )
    self.code_lines = VGroup(*rows).move_to(ORIGIN)
    """The lines, a part: its i-th part is the i-th line, a group of its glyphs."""

    if add_line_numbers:
        numbers = range(line_numbers_from, line_numbers_from + len(lines))
        self.line_numbers = VGroup(
            *_typeset([str(n) for n in numbers], text, style.line_numbers)
        ).move_to(ORIGIN)
        """The line numbers, a part (if the lines are numbered): its i-th part is
        the i-th line's number, lined up on the right."""
        right = self.line_numbers.get_right()[0]
        for row in self.line_numbers.submobjects:
            row.shift(RIGHT * (right - row.get_right()[0]))
        self.line_numbers.next_to(self.code_lines, LEFT)
        first = VGroup(*rows[0].submobjects[-len(str(line_numbers_from)) :])
        self.line_numbers.shift(
            UP * (first.get_y() - self.line_numbers.submobjects[0].get_y())
        )
        self.add(self.line_numbers)

    # what CE keeps of the closings: their height, on the left edge of their glyphs and of
    # the space before each, which CE's Text puts on the glyph before it
    boundary = sorted({0, len(rows) - 1})
    glyphs = [glyph for row in rows for glyph in row.submobjects]
    closings = [g for i in boundary for g in rows[i].submobjects[-closing:]]
    spaces = []
    for i in boundary:
        before = sum(len(row.submobjects) for row in rows[: i + 1]) - closing
        spaces.append(
            glyphs[before - 1] if before else rows[i].submobjects[-closing]
        )
    edge = min(
        [g.get_center()[0] for g in spaces] + [g.get_left()[0] for g in closings]
    )
    extent = Line(
        [edge, min(g.get_bottom()[1] for g in closings), 0],
        [edge, max(g.get_top()[1] for g in closings), 0],
    )
    for i in boundary:
        rows[i].remove(*rows[i].submobjects[-closing:])
    self.add(self.code_lines)

    config = self.default_background_config | (background_config or {})
    if config.get("fill_color") is None:
        config["fill_color"] = style.background
    listing = VGroup(*self.submobjects, extent)
    if background == "rectangle":
        self.background = SurroundingRectangle(listing, **config)
        """The rectangle behind the listing, its first part; a window's buttons are
        its submobjects."""
    elif background == "window":
        buttons = VGroup(
            *(
                Dot(radius=0.1, stroke_width=0, color=c)
                for c in ["#ff5f56", "#ffbd2e", "#27c93f"]
            )
        ).arrange(RIGHT, buff=0.1)
        buttons.next_to(listing, UP, buff=0.1).align_to(listing, LEFT).shift(
            LEFT * 0.1
        )
        self.background = SurroundingRectangle(listing, buttons, **config)
        buttons.shift(UP * 0.1 + LEFT * 0.1)
        self.background.add(buttons)
    else:
        raise ValueError(f"Unknown background type: {background}")
    self.add_to_back(self.background)

default_background_config

The background's keywords unless background_config changes them; its fill is the style's background color unless a fill_color is given.

default_paragraph_config

The text's keywords unless paragraph_config changes them.

code_lines

The lines, a part: its i-th part is the i-th line, a group of its glyphs.

line_numbers

The line numbers, a part (if the lines are numbered): its i-th part is the i-th line's number, lined up on the right.

background

The rectangle behind the listing, its first part; a window's buttons are its submobjects.

CodeText

The text of a Code listing: its paragraph_config, the text keywords that apply to a listing.

font

The font family: "Monospace", the default, is Menlo where the system has it, and elsewhere DejaVu Sans Mono, which manimgx ships; any other family is one manimgx ships, or else the system's.

font_size

The size of the text: an em is font_size / 72 scene units (default 24).

line_spacing

How far apart the lines are, beyond the font size: their baselines are (1 + line_spacing) * font_size / 96 scene units apart (default 0.5; -1 for 0.3).

disable_ligatures

Whether every character is a glyph of its own, with no ligatures (default True).

font

The font family: "Monospace", the default, is Menlo where the system has it, and elsewhere DejaVu Sans Mono, which manimgx ships; any other family is one manimgx ships, or else the system's.

font_size

The size of the text: an em is font_size / 72 scene units (default 24).

line_spacing

How far apart the lines are, beyond the font size: their baselines are (1 + line_spacing) * font_size / 96 scene units apart (default 0.5; -1 for 0.3).

disable_ligatures

Whether every character is a glyph of its own, with no ligatures (default True).

Weights and slants

A text's weight is how heavy its strokes are, and its slant how it leans.

m.NORMAL

Text's normal slant, upright, and its normal weight (400).

m.ITALIC

An italic slant of text.

m.OBLIQUE

An oblique slant of text: its upright letters, slanted.

m.THIN

A weight of text: thin (100).

m.ULTRALIGHT

A weight of text: ultralight (200).

m.LIGHT

A weight of text: light (300).

m.SEMILIGHT

A weight of text: semilight (350).

m.BOOK

A weight of text: book (380).

m.MEDIUM

A weight of text: medium (500).

m.SEMIBOLD

A weight of text: semibold (600).

m.BOLD

A weight of text: bold (700).

m.ULTRABOLD

A weight of text: ultrabold (800).

m.HEAVY

A weight of text: heavy (900).

m.ULTRAHEAVY

A weight of text: ultraheavy (950).