Skip to content

A circle draws sine and cosine

A rotating radius traces sine and cosine from its height and reach. The right triangle inside the circle also shows why sin²θ + cos²θ = 1.

examples/unit_circle.py
"""Sine and cosine are a circle turning.

Turn a radius of the unit circle by an angle θ. Its tip is (cos θ, sin θ): cos θ is how far it
reaches across, sin θ how high it rises, the two legs of a right triangle whose hypotenuse is
the radius. Carry the height to the right as θ grows and it draws the sine wave; the reach
draws the cosine, the same wave a quarter turn ahead. And since the legs and the radius make a
right triangle, sin²θ + cos²θ = 1 at every angle.
"""

import numpy as np

import manimgx as m

WALL = 3.0  # the README wall's 5 seconds start here
CENTER = np.array([-4.55, 0.35, 0.0])  # the circle's
RADIUS = 1.45
ORIGIN = np.array([-2.35, 0.35, 0.0])  # where θ = 0 on the waves' axis
LENGTH = 9.0  # the waves' axis, one turn long
SINE, COSINE = m.YELLOW, m.GREEN


def on_wave(theta: float, value: float) -> np.ndarray:
    return ORIGIN + np.array([LENGTH * theta / m.TAU, RADIUS * value, 0.0])


class UnitCircle(m.Scene):
    def construct(self) -> None:
        theta = m.ValueTracker(0.0)
        circle = m.Circle(radius=RADIUS, stroke_color=m.GREY_B, stroke_width=4).move_to(
            CENTER
        )
        across = m.Line(
            CENTER + 1.25 * RADIUS * m.LEFT,
            CENTER + 1.25 * RADIUS * m.RIGHT,
            stroke_width=2,
            stroke_color=m.GREY_D,
        )
        upright = m.Line(
            CENTER + 1.25 * RADIUS * m.DOWN,
            CENTER + 1.25 * RADIUS * m.UP,
            stroke_width=2,
            stroke_color=m.GREY_D,
        )
        axis = m.Arrow(
            ORIGIN,
            ORIGIN + (LENGTH + 0.5) * m.RIGHT,
            buff=0,
            stroke_width=3,
            color=m.GREY_B,
            tip_length=0.2,
        )
        ticks = m.VGroup()
        for k, tex in enumerate(
            [r"\frac{\pi}{2}", r"\pi", r"\frac{3\pi}{2}", r"2\pi"], start=1
        ):
            at = ORIGIN + LENGTH * k / 4 * m.RIGHT
            ticks.add(
                m.Line(
                    at + 0.1 * m.DOWN, at + 0.1 * m.UP, stroke_width=3, color=m.GREY_B
                )
            )
            ticks.add(
                m.MathTex(tex, font_size=34, color=m.GREY_A).next_to(
                    at, m.DOWN, buff=0.22
                )
            )
        for value in (1, -1):
            at = ORIGIN + RADIUS * value * m.UP
            ticks.add(
                m.DashedLine(
                    at,
                    at + LENGTH * m.RIGHT,
                    stroke_width=1.5,
                    stroke_opacity=0.5,
                    color=m.GREY_D,
                )
            )

        def tip() -> np.ndarray:
            a = theta.get_value()
            return CENTER + RADIUS * np.array([np.cos(a), np.sin(a), 0.0])

        def foot() -> np.ndarray:
            return np.array([tip()[0], CENTER[1], 0.0])

        radius = m.always_redraw(
            lambda: m.Line(CENTER, tip(), stroke_width=4, color=m.WHITE)
        )
        cos_leg = m.always_redraw(
            lambda: m.Line(CENTER, foot(), stroke_width=7, color=COSINE)
        )
        sin_leg = m.always_redraw(
            lambda: m.Line(foot(), tip(), stroke_width=7, color=SINE)
        )
        dot = m.always_redraw(lambda: m.Dot(tip(), radius=0.09, color=m.WHITE))

        def angle() -> float:
            """θ, from 0 to a whole turn, then on into the next turn from 0 again."""
            a = theta.get_value()
            return max(a if a <= m.TAU + 1e-9 else a - m.TAU, 1e-4)

        arc = m.always_redraw(
            lambda: m.Arc(
                radius=0.38,
                start_angle=0,
                angle=angle(),
                arc_center=CENTER,
                stroke_width=3,
                color=m.GREY_A,
            )
        )
        theta_label = m.always_redraw(
            lambda: m.MathTex(r"\theta", font_size=36).move_to(
                CENTER + 0.62 * np.array([np.cos(angle() / 2), np.sin(angle() / 2), 0])
            )
        )

        def wave(f: np.ufunc, color: str) -> m.Mobject:
            def draw() -> m.VMobject:
                a = max(theta.get_value(), 1e-3)
                return m.ParametricFunction(
                    lambda s: on_wave(s, f(s)),
                    t_range=[0, a, a / 120],
                    stroke_width=6,
                    color=color,
                )

            return m.always_redraw(draw)

        sine, cosine = wave(np.sin, SINE), wave(np.cos, COSINE)
        sine_head = m.always_redraw(
            lambda: m.Dot(
                on_wave(theta.get_value(), np.sin(theta.get_value())),
                radius=0.08,
                color=SINE,
            )
        )
        cosine_head = m.always_redraw(
            lambda: m.Dot(
                on_wave(theta.get_value(), np.cos(theta.get_value())),
                radius=0.08,
                color=COSINE,
            )
        )
        carry = m.always_redraw(
            lambda: m.DashedLine(
                tip(),
                on_wave(theta.get_value(), np.sin(theta.get_value())),
                stroke_width=2.5,
                color=SINE,
                dash_length=0.12,
            )
        )

        self.play(
            m.Create(circle),
            m.FadeIn(across, upright),
            m.GrowArrow(axis),
            m.FadeIn(ticks),
            run_time=1.5,
        )
        self.play(
            m.FadeIn(
                radius, cos_leg, sin_leg, dot, arc, theta_label, sine_head, cosine_head
            ),
            run_time=0.6,
        )
        self.add(sine, cosine, carry)
        self.play(theta.animate.set_value(m.TAU), run_time=10, rate_func=m.linear)

        names = m.VGroup(  # by each wave's first peak
            m.MathTex(r"\sin\theta", font_size=48, color=SINE).next_to(
                on_wave(np.pi / 2, 1), m.UP, buff=0.25
            ),
            m.MathTex(r"\cos\theta", font_size=48, color=COSINE).next_to(
                on_wave(np.pi, -1), m.DOWN, buff=0.25
            ),
        )
        identity = m.MathTex(
            r"{{ \sin^2\theta }} + {{ \cos^2\theta }} = 1", font_size=56
        )
        identity.set_color_by_tex(r"\sin^2", SINE).set_color_by_tex(r"\cos^2", COSINE)
        identity.move_to(CENTER + 2.75 * m.DOWN)
        for traced in (sine, cosine):
            traced.clear_updaters()  # the waves stay as one turn drew them
        self.play(
            m.FadeIn(names), m.FadeOut(carry, sine_head, cosine_head), run_time=0.8
        )
        self.play(theta.animate.set_value(m.TAU + 0.9), run_time=1.2)
        self.play(
            m.TransformFromCopy(sin_leg, identity[0]),
            m.TransformFromCopy(cos_leg, identity[2]),
            m.FadeIn(identity[1], identity[3]),
            run_time=1.6,
        )
        self.wait(2)


if __name__ == "__main__":
    UnitCircle().render("unit_circle.mp4")