Skip to content

draw

champalimaud.draw

Small manim helpers that draw stills.

render_still turns mobjects into a PNG. The other helpers build the mobjects a network diagram needs. No data and no network. Importing this module sets where manim writes its files.

arrow(src, dst, color, *, slot=(0, 1), dashed=False)

Make an arrow from the rim of one node to the rim of another.

Arrows that end on one node take different slots, so their heads land at different heights.

Parameters:

Name Type Description Default
src VGroup

Nodes, as node returns them; only the circle at index 0 is read.

required
dst VGroup

Nodes, as node returns them; only the circle at index 0 is read.

required
color str

Color of the arrow.

required
slot tuple of int

(n, of): this is the n-th of of arrows that end on dst.

(0, 1)
dashed bool

Draw a dashed line, to mark a connection that is set aside.

False

Returns:

Type Description
Arrow or DashedLine

The arrow, or a dashed line with the same head.

Examples:

>>> import manim as mn
>>> a = mn.VGroup(mn.Circle(radius=0.4)).move_to([-3, 0, 0])
>>> b = mn.VGroup(mn.Circle(radius=0.4)).move_to([3, 0, 0])
>>> round(float(arrow(a, b, "#000000").get_end()[0]), 2)
2.6
Source code in champalimaud/draw.py
def arrow(
    src: mn.VGroup,
    dst: mn.VGroup,
    color: str,
    *,
    slot: tuple[int, int] = (0, 1),
    dashed: bool = False,
) -> mn.Arrow | mn.DashedLine:
    """Make an arrow from the rim of one node to the rim of another.

    Arrows that end on one node take different slots, so their heads
    land at different heights.

    Parameters
    ----------
    src, dst : manim.VGroup
        Nodes, as `node` returns them; only the circle at index 0 is
        read.
    color : str
        Color of the arrow.
    slot : tuple of int, default (0, 1)
        ``(n, of)``: this is the n-th of `of` arrows that end on `dst`.
    dashed : bool, default False
        Draw a dashed line, to mark a connection that is set aside.

    Returns
    -------
    manim.Arrow or manim.DashedLine
        The arrow, or a dashed line with the same head.

    Examples
    --------
    >>> import manim as mn
    >>> a = mn.VGroup(mn.Circle(radius=0.4)).move_to([-3, 0, 0])
    >>> b = mn.VGroup(mn.Circle(radius=0.4)).move_to([3, 0, 0])
    >>> round(float(arrow(a, b, "#000000").get_end()[0]), 2)
    2.6
    """
    n, of = slot
    a, b = src[0].get_center(), dst[0].get_center()
    r_a, r_b = src[0].width / 2, dst[0].width / 2
    heights = np.linspace(0.55 * r_b, -0.55 * r_b, of) if of > 1 else [0]
    aim = b + np.array([0, heights[n], 0])
    toward = (aim - a) / np.linalg.norm(aim - a)
    # The line from a toward aim first meets dst's circle here.
    along = (b - a) @ toward
    off = np.linalg.norm((b - a) - along * toward)
    end = a + (along - np.sqrt(r_b**2 - off**2)) * toward
    start = a + (r_a + 0.06) * toward
    if dashed:
        return mn.DashedLine(
            start, end, color=color, stroke_width=4, dash_length=0.12
        ).add_tip(tip_length=0.22, tip_width=0.22)
    return mn.Arrow(
        start,
        end,
        buff=0,
        color=color,
        stroke_width=4,
        tip_length=0.22,
        max_tip_length_to_length_ratio=1,
    )

bracket(x, top, bottom, color)

Make a thin bracket, open to the right, to mark a group of nodes.

Parameters:

Name Type Description Default
x float

Horizontal position of the bracket's spine.

required
top float

Vertical extent.

required
bottom float

Vertical extent.

required
color str

Color of the stroke.

required

Returns:

Type Description
VMobject

The bracket.

Examples:

>>> bracket(1, 2, 0, "#000000").get_start().tolist()
[1.1, 2.0, 0.0]
Source code in champalimaud/draw.py
def bracket(x: float, top: float, bottom: float, color: str) -> mn.VMobject:
    """Make a thin bracket, open to the right, to mark a group of nodes.

    Parameters
    ----------
    x : float
        Horizontal position of the bracket's spine.
    top, bottom : float
        Vertical extent.
    color : str
        Color of the stroke.

    Returns
    -------
    manim.VMobject
        The bracket.

    Examples
    --------
    >>> bracket(1, 2, 0, "#000000").get_start().tolist()
    [1.1, 2.0, 0.0]
    """
    corners = [(x + 0.1, top), (x, top), (x, bottom), (x + 0.1, bottom)]
    return mn.VMobject(color=color, stroke_width=2).set_points_as_corners(
        np.array([[cx, cy, 0] for cx, cy in corners])
    )

fade(mobject, opacity=0.4)

Dim the strokes and fills of a mobject.

Shapes without a fill stay without one; set_opacity would fill an arc and turn it into a D.

Parameters:

Name Type Description Default
mobject Mobject

What to dim; changed in place.

required
opacity float

Opacity of the strokes and of the fills that exist.

0.4

Returns:

Type Description
Mobject

mobject.

Examples:

>>> import manim as mn
>>> circle = fade(mn.Circle(), 0.25)
>>> round(float(circle.get_stroke_opacity()), 2)
0.25
>>> float(circle.get_fill_opacity())
0.0
Source code in champalimaud/draw.py
def fade(mobject: mn.Mobject, opacity: float = 0.4) -> mn.Mobject:
    """Dim the strokes and fills of a mobject.

    Shapes without a fill stay without one; ``set_opacity`` would fill
    an arc and turn it into a D.

    Parameters
    ----------
    mobject : manim.Mobject
        What to dim; changed in place.
    opacity : float, default 0.4
        Opacity of the strokes and of the fills that exist.

    Returns
    -------
    manim.Mobject
        `mobject`.

    Examples
    --------
    >>> import manim as mn
    >>> circle = fade(mn.Circle(), 0.25)
    >>> round(float(circle.get_stroke_opacity()), 2)
    0.25
    >>> float(circle.get_fill_opacity())
    0.0
    """
    for part in mobject.family_members_with_points():
        part.set_stroke(opacity=opacity)
        if part.get_fill_opacity() > 0:
            part.set_fill(opacity=opacity)
    return mobject

label_on(line, text, color, *, at=0.3, shift=(0, 0))

Place a LaTeX label on a white patch along a straight line.

The patch keeps lines that cross the label from running through the text.

Parameters:

Name Type Description Default
line Mobject

A straight line or arrow.

required
text str

LaTeX for the label.

required
color str

Color of the text.

required
at float

Fraction of the way along the line.

0.3
shift tuple of float

Offset of the label, as (x, y).

(0, 0)

Returns:

Type Description
VGroup

The patch at index 0 and the label at index 1.

Source code in champalimaud/draw.py
def label_on(
    line: mn.Mobject,
    text: str,
    color: str,
    *,
    at: float = 0.3,
    shift: tuple[float, float] = (0, 0),
) -> mn.VGroup:
    """Place a LaTeX label on a white patch along a straight line.

    The patch keeps lines that cross the label from running through
    the text.

    Parameters
    ----------
    line : manim.Mobject
        A straight line or arrow.
    text : str
        LaTeX for the label.
    color : str
        Color of the text.
    at : float, default 0.3
        Fraction of the way along the line.
    shift : tuple of float, default (0, 0)
        Offset of the label, as ``(x, y)``.

    Returns
    -------
    manim.VGroup
        The patch at index 0 and the label at index 1.
    """
    label = mn.MathTex(text, color=color, font_size=30).move_to(
        (line.get_start() + at * (line.get_end() - line.get_start()))
        + np.append(shift, 0.0)[:3]
    )
    return mn.VGroup(
        mn.BackgroundRectangle(
            label, color=mn.WHITE, fill_opacity=0.9, buff=0.04
        ),
        label,
    )

node(label, at, color, *, radius=0.3, ring=None)

Make a circle with a LaTeX label.

The circle is solid in color with white text or, given a ring width, a paper circle with a ring of color. The text scales with the circle, so a label always fits.

Parameters:

Name Type Description Default
label str

LaTeX for the text.

required
at tuple of float

Center, as (x, y).

required
color str

Fill, or ring color.

required
radius float

Radius of the circle.

0.3
ring float

Stroke width of the ring; None draws a solid node.

None

Returns:

Type Description
VGroup

The circle at index 0 and the label at index 1.

Source code in champalimaud/draw.py
def node(
    label: str,
    at: tuple[float, float],
    color: str,
    *,
    radius: float = 0.3,
    ring: float | None = None,
) -> mn.VGroup:
    """Make a circle with a LaTeX label.

    The circle is solid in `color` with white text or, given a ring
    width, a paper circle with a ring of `color`.
    The text scales with the circle, so a label always fits.

    Parameters
    ----------
    label : str
        LaTeX for the text.
    at : tuple of float
        Center, as ``(x, y)``.
    color : str
        Fill, or ring color.
    radius : float, default 0.3
        Radius of the circle.
    ring : float, optional
        Stroke width of the ring; ``None`` draws a solid node.

    Returns
    -------
    manim.VGroup
        The circle at index 0 and the label at index 1.
    """
    circle = mn.Circle(
        radius=radius,
        color=color,
        stroke_width=ring or 4,
        fill_color=color if ring is None else PAPER,
        fill_opacity=1,
    )
    text = mn.MathTex(
        label,
        color=mn.WHITE if ring is None else INK,
        font_size=100 * radius,
    )
    return mn.VGroup(circle, text).move_to(np.append(at, 0.0)[:3])

render_still(*mobjects, width=9.0, height=7.0, per_unit=150)

Render mobjects as a PNG on a white frame.

Parameters:

Name Type Description Default
*mobjects Mobject

What to draw.

()
width float

Size of the frame, in manim units.

9.0 and 7.0
height float

Size of the frame, in manim units.

9.0 and 7.0
per_unit int

Pixels per unit.

150

Returns:

Type Description
bytes

The PNG file.

Source code in champalimaud/draw.py
def render_still(
    *mobjects: mn.Mobject,
    width: float = 9.0,
    height: float = 7.0,
    per_unit: int = 150,
) -> bytes:
    """Render mobjects as a PNG on a white frame.

    Parameters
    ----------
    *mobjects : manim.Mobject
        What to draw.
    width, height : float, default 9.0 and 7.0
        Size of the frame, in manim units.
    per_unit : int, default 150
        Pixels per unit.

    Returns
    -------
    bytes
        The PNG file.
    """

    class Still(mn.Scene):
        def construct(self):
            self.add(*mobjects)

    with mn.tempconfig(
        {
            "background_color": mn.WHITE,
            "frame_width": width,
            "frame_height": height,
            "pixel_width": int(width * per_unit),
            "pixel_height": int(height * per_unit),
            "save_last_frame": True,
            "output_file": "still",
            "progress_bar": "none",
        }
    ):
        Still().render()
    newest = max(
        CACHE.glob("images/**/still*.png"), key=lambda p: p.stat().st_mtime
    )
    return newest.read_bytes()