圆周画出正弦与余弦¶
旋转的半径从其高度和跨度描出 正弦和余弦。圆内的直角三角形也说明了为何 sin²θ + cos²θ = 1。
examples/unit_circle.py
"""正弦与余弦即旋转的圆周。
将单位圆的一条半径转动角度 θ。其端点为 (cos θ, sin θ):cos θ 是它横向
到达的距离,sin θ 是它升起的高度,二者是一个以半径为斜边的直角三角形的
两条直角边。随着 θ 增长将高度向右搬运,便画出正弦波;到达距离
画出余弦,即同一波形超前四分之一转。由于两直角边与半径构成
直角三角形,在每个角度都有 sin²θ + cos²θ = 1。
"""
import numpy as np
import manimgx as m
WALL = 3.0 # README 影片墙的 5 秒从此开始
CENTER = np.array([-4.55, 0.35, 0.0]) # 圆的
RADIUS = 1.45
ORIGIN = np.array([-2.35, 0.35, 0.0]) # 波形轴上 θ = 0 的位置
LENGTH = 9.0 # 波形轴,一个完整转的长度
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:
"""θ,从 0 到一整转,而后从 0 再次进入下一转。"""
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( # 在每个波的第一个峰处
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() # 波形保持一整转所绘的样子
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")