Skip to content

Complex functions bend the plane

A grid bends under z², the exponential and the sine. Straight lines become parabolas, circles and rays as each point travels to its image.

examples/complex_maps.py
"""Complex functions bend the plane.

A complex function sends each point z of the plane to a point f(z). Carry a grid along and the
function's shape appears. z² doubles every angle about the origin and squares every distance:
the grid's lines become parabolas. e^{x+iy} = e^x · e^{iy}, so a vertical line wraps onto a
circle and a horizontal line straightens into a ray. sin z bends the lines into ellipses and
hyperbolas that share two foci. Whatever the function, the bent lines still cross at right
angles: a complex function keeps angles wherever its derivative isn't 0.
"""

from collections.abc import Callable

import numpy as np

import manimgx as m

WALL = 2.4  # the README wall's 5 seconds start here
UNIT = 1.75  # screen units per unit of the plane
HALF = 2.0  # the grid covers [−HALF, HALF] in both directions
STEP = 0.25  # between its lines
SAMPLES = 240  # points along each line
REAL, IMAGINARY = m.BLUE_D, m.YELLOW


def grid() -> tuple[m.VGroup, list[np.ndarray]]:
    """The grid's lines, and each line's points as complex numbers."""
    lines = m.VGroup()
    points: list[np.ndarray] = []
    run = np.linspace(-HALF, HALF, SAMPLES)
    for k in np.arange(-HALF, HALF + STEP / 2, STEP):
        for z, color in ((k + 1j * run, REAL), (run + 1j * k, IMAGINARY)):
            axis = abs(k) < 1e-9
            line = m.VMobject(
                stroke_color=m.WHITE if axis else color,
                stroke_width=4.5 if axis else 3,
            )
            lines.add(line)
            points.append(z)
    return lines, points


def place(
    lines: m.VGroup,
    points: list[np.ndarray],
    f: Callable[[np.ndarray], np.ndarray],
    t: float,
) -> None:
    """Each line at t of the way from z to f(z)."""
    for line, z in zip(lines, points, strict=True):
        assert isinstance(line, m.VMobject)
        w = (1 - t) * z + t * f(z)
        line.set_points_as_corners(
            UNIT * np.stack([w.real, w.imag, np.zeros(len(w))], axis=1)
        )


class ComplexMaps(m.Scene):
    def construct(self) -> None:
        lines, points = grid()
        place(lines, points, lambda z: z, 0)
        ghost = lines.copy().set_stroke(opacity=0.18)  # where the lines started
        corner = np.array([-6.6, 3.45, 0.0])
        shown = {"label": m.MathTex("f(z) = z", font_size=60)}
        shown["label"].move_to(corner, aligned_edge=m.LEFT)

        def relabel(tex: str, seconds: float) -> None:
            formula = m.MathTex(tex, font_size=60).move_to(corner, aligned_edge=m.LEFT)
            self.play(m.TransformMatchingTex(shown["label"], formula), run_time=seconds)
            shown["label"] = formula

        def bend(
            f: Callable[[np.ndarray], np.ndarray], back: bool, seconds: float
        ) -> None:
            self.play(
                m.UpdateFromAlphaFunc(
                    lines, lambda mob, a: place(mob, points, f, 1 - a if back else a)
                ),
                run_time=seconds,
            )

        def show(f: Callable[[np.ndarray], np.ndarray], tex: str) -> None:
            relabel(tex, 0.8)
            bend(f, back=False, seconds=3)
            self.wait(1.2)
            bend(f, back=True, seconds=2)
            relabel("f(z) = z", 0.6)

        self.add(ghost)
        self.play(
            m.Create(lines, lag_ratio=0.01), m.Write(shown["label"]), run_time=1.5
        )
        show(lambda z: z * z, "f(z) = z^2")
        show(np.exp, "f(z) = e^z")
        show(np.sin, r"f(z) = \sin z")
        self.wait(1)


if __name__ == "__main__":
    ComplexMaps().render("complex_maps.mp4")