Skip to content

Times tables on a circle

Joining numbers to their multiples around a circle draws cardioids and other curves. Changing the multiplier continuously makes the pattern turn, split and fold.

examples/times_tables.py
"""Times tables on a circle.

Put 200 points around a circle, numbered 0 to 199, and join each point n to the point k·n,
counting around the circle (mod 200). For k = 2 the lines wrap a cardioid, the heart traced by
a point on a circle rolling around another of the same size; for k = 3, a nephroid, with two
lobes; for k, a curve with k − 1 lobes. Let k grow continuously and the figure turns, splits
and folds from one curve into the next.
"""

import numpy as np

import manimgx as m

WALL = 3.2  # the README wall's 5 seconds start here
POINTS = 200
RADIUS = 3.45


def on_circle(angle: np.ndarray) -> np.ndarray:
    return RADIUS * np.stack(
        [np.cos(angle), np.sin(angle), np.zeros_like(angle)], axis=-1
    )


class TimesTables(m.Scene):
    def construct(self) -> None:
        k = m.ValueTracker(2.0)
        n = np.arange(POINTS)
        start = np.pi + m.TAU * n / POINTS  # point 0 on the left, as in a clock turned
        colors = m.color_gradient(
            [m.BLUE, m.TEAL, m.GREEN, m.YELLOW, m.RED, m.PURPLE, m.BLUE], POINTS
        )
        chords = m.VGroup(
            *(
                m.Line(m.LEFT, m.RIGHT, stroke_width=2, stroke_opacity=0.75, color=c)
                for c in colors
            )
        )

        def draw(group: m.VGroup) -> None:
            ends = on_circle(np.pi + m.TAU * k.get_value() * n / POINTS)
            begins = on_circle(start)
            for line, a, b in zip(group, begins, ends, strict=True):
                assert isinstance(line, m.Line)
                line.set_points_as_corners([a, b])

        chords.add_updater(draw)
        draw(chords)
        ring = m.Circle(radius=RADIUS, stroke_color=m.GREY_B, stroke_width=3)
        dots = m.VGroup(
            *(m.Dot(p, radius=0.022, color=m.GREY_A) for p in on_circle(start))
        )

        rule = m.MathTex(r"n \mapsto k \cdot n \pmod{200}", font_size=46)
        value = m.DecimalNumber(2.0, num_decimal_places=2, font_size=60, color=m.YELLOW)
        k_label = m.MathTex("k =", font_size=60)
        readout = m.VGroup(k_label, value).arrange(m.RIGHT, buff=0.2)
        panel = m.VGroup(rule, readout).arrange(m.DOWN, aligned_edge=m.LEFT, buff=0.35)
        panel.to_corner(m.UL, buff=0.5)
        value.add_updater(lambda d: d.set_value(k.get_value()))

        names = {2: "cardioid", 3: "nephroid", 4: "three lobes", 5: "four lobes"}
        name = m.Tex(names[2], font_size=48, color=m.GREY_A).next_to(
            panel, m.DOWN, aligned_edge=m.LEFT, buff=0.4
        )

        self.play(m.Create(ring), m.FadeIn(dots), run_time=1)
        self.play(m.Create(chords, lag_ratio=0.01), m.Write(panel), run_time=1.8)
        self.play(m.FadeIn(name), run_time=0.4)
        for target in (3, 4, 5):
            self.play(
                k.animate.set_value(target),
                m.FadeOut(name),
                run_time=2.4,
            )
            name = m.Tex(names[target], font_size=48, color=m.GREY_A).move_to(
                name, aligned_edge=m.LEFT
            )
            self.play(m.FadeIn(name), run_time=0.4)
            self.wait(0.6)
        self.play(m.FadeOut(name), run_time=0.3)
        # then on, without stopping: the figures between and beyond
        self.play(k.animate.set_value(34), run_time=12, rate_func=m.linear)
        self.wait(1)


if __name__ == "__main__":
    TimesTables().render("times_tables.mp4")