# 示范片：用 Manim 讲「梯度下降」——AI 是怎么一步步学会的
#   ~/mvenv/bin/manim -qh --fps 30 descent.py Descent2D Descent3D
# 时间点读 beats.json（beats.py 从配音的逐字时间算出来，单位秒，从示范片开头算起）；没有就用默认值。
# 先配音、再按这些时间点写动画：self.until(B["step"]) 就是「等到念『走过去』的那一刻，再迈这一步」。
import json
from pathlib import Path

import numpy as np
from manim import *

BG = "#0E1117"
CURVE = "#58C4DD"   # 蓝：误差（曲线、曲面）
DOT = "#FFD54A"     # 黄：当前的参数（点、步子）
TAN = "#FF7A59"     # 橙：斜率（切线、公式里的「斜率」）
GOOD = "#C6F432"    # 黄绿：步长 η
MUTED = "#8A93A6"
INK = "#ECEFF4"
CJK = "Noto Sans CJK SC"
config.background_color = BG

DEFAULT = dict(
    curve=1.0, xlab=4.7, down=6.0, dot=8.2, tangent=10.7, w1=13.1, w2=15.6, w3=17.5, rule=19.4, eta=23.4,
    plug0=26.4, plug1=27.4, plug2=28.4, plug3=30.1, step=31.9, loop=32.8, shrink=37.3, lr=39.8,
    small=43.1, good=46.0, big=47.9, end2d=51.5,
    lift=54.7, path=57.6, many=61.3, many2=62.2, many3=66.8, end=69.6,
)
_bf = Path(__file__).with_name("beats.json")
B = {**DEFAULT, **(json.loads(_bf.read_text()) if _bf.exists() else {})}
T3 = B["end2d"] + 1.2  # 第二场（3D）从示范片的第几秒开始 = 第一场的长度


def L(w):
    return 0.22 * (w - 0.4) ** 2 + 0.3 + 0.12 * np.cos(1.7 * w)


def dL(w):
    return 0.44 * (w - 0.4) - 0.204 * np.sin(1.7 * w)


def zh(s, size=36, color=INK, weight=BOLD):
    # LaTeX 默认不认中文：中文都用 Text（系统字体），再跟 MathTex 拼在一起
    return Text(s, font=CJK, font_size=size, color=color, weight=weight)


def descend(w0, lr, n):
    ws = [w0]
    for _ in range(n):
        ws.append(ws[-1] - lr * dL(ws[-1]))
    return ws


class Clocked(Scene):
    offset = 0.0

    def until(self, t):
        """等到示范片的第 t 秒（动画已经超过就不等）"""
        t -= self.offset
        if t - self.time > 1 / 60:
            self.wait(t - self.time)


class Descent2D(Clocked, MovingCameraScene):
    def construct(self):
        ax = Axes(x_range=[-3.5, 3.5, 1], y_range=[0, 4, 1], x_length=10.5, y_length=4.2,
                  axis_config=dict(color=MUTED, stroke_width=2, include_ticks=False, tip_width=0.18, tip_height=0.18))
        ax.shift(DOWN * 1.35)
        xl = zh("参数 w", 30, MUTED).next_to(ax.x_axis.get_end(), DOWN, buff=0.25).shift(LEFT * 0.5)
        yl = zh("误差", 30, MUTED).next_to(ax.y_axis.get_end(), RIGHT, buff=0.2)
        graph = ax.plot(L, x_range=[-3.4, 3.4], color=CURVE, stroke_width=5)

        self.until(B["curve"])
        self.play(Create(ax), FadeIn(yl, shift=RIGHT * 0.2), run_time=1.0)
        self.play(Create(graph), run_time=2.0, rate_func=smooth)
        self.until(B["xlab"])
        self.play(FadeIn(xl, shift=UP * 0.2), run_time=0.4)
        self.play(Indicate(xl, color=INK, scale_factor=1.15), run_time=0.6)

        # 越往下越准：沿着纵轴往下指
        self.until(B["down"])
        top, bot = ax.c2p(-3.2, 3.2), ax.c2p(-3.2, 0.5)
        down = Arrow(top, bot, buff=0, color=MUTED, stroke_width=5, max_tip_length_to_length_ratio=0.12).shift(LEFT * 0.4)
        down_lbl = zh("越低越准", 26, MUTED).next_to(down, LEFT, buff=0.15)
        self.play(GrowArrow(down), FadeIn(down_lbl), run_time=0.6)

        # 一个点：当前的参数。它的位置由 w 这个「旋钮」决定，切线和斜率都跟着它重画
        w = ValueTracker(-2.8)
        dot = always_redraw(lambda: Dot(ax.input_to_graph_point(w.get_value(), graph), radius=0.12, color=DOT).set_z_index(5))
        glow = always_redraw(lambda: Dot(dot.get_center(), radius=0.28, color=DOT, fill_opacity=0.18).set_z_index(4))

        self.until(B["dot"])
        self.play(FadeOut(down), FadeOut(down_lbl), run_time=0.3)
        start = ax.input_to_graph_point(-2.8, graph)
        drop = Dot(start + UP * 2.2, radius=0.12, color=DOT)
        self.play(FadeIn(drop, shift=DOWN * 0.3), run_time=0.25)
        self.play(drop.animate.move_to(start), rate_func=rate_functions.ease_in_quad, run_time=0.45)
        self.remove(drop)
        self.add(glow, dot)
        self.play(Flash(start, color=DOT, line_length=0.25, num_lines=10, flash_radius=0.35), run_time=0.6)

        def tangent():
            x = w.get_value()
            p = ax.c2p(x, L(x))
            q = ax.c2p(x + 1, L(x) + dL(x))
            d = np.array([q[0] - p[0], q[1] - p[1], 0])
            d = d / np.linalg.norm(d)
            return Line(p - d * 1.5, p + d * 1.5, color=TAN, stroke_width=5).set_z_index(3)

        tan = always_redraw(tangent)
        slope = DecimalNumber(dL(-2.8), num_decimal_places=2, include_sign=True, font_size=44, color=TAN)
        slope_lbl = VGroup(zh("斜率", 32, TAN), slope).arrange(RIGHT, buff=0.18)
        slope_lbl.add_updater(lambda m: m.next_to(dot, RIGHT, buff=0.45).shift(UP * 0.3))
        slope.add_updater(lambda m: m.set_value(dL(w.get_value())))

        self.until(B["tangent"])
        self.play(Create(tan), run_time=0.7)
        self.play(FadeIn(slope_lbl, shift=LEFT * 0.2), run_time=0.4)

        # 拧一拧旋钮：坡往下倾是负的，往上翘是正的，越陡数字越大
        self.until(B["w1"])
        self.play(w.animate.set_value(-1.2), run_time=1.6, rate_func=smooth)
        self.until(B["w2"])
        self.play(w.animate.set_value(2.5), run_time=1.6, rate_func=smooth)
        self.until(B["w3"])
        self.play(w.animate.set_value(-2.8), run_time=1.5, rate_func=smooth)

        # 更新规则：往斜率的反方向走一小步
        self.until(B["rule"])
        rule = VGroup(MathTex(r"w", r"\;\leftarrow\;", r"w", r"\;-\;", r"\eta", r"\times", font_size=64),
                      zh("斜率", 48, TAN)).arrange(RIGHT, buff=0.18)
        rule[0][4].set_color(GOOD)
        rule.to_corner(UR, buff=0.55)
        self.play(Write(rule[0]), FadeIn(rule[1], shift=UP * 0.2), run_time=1.4)
        self.until(B["eta"])
        eta_note = zh("η：步子迈多大", 28, GOOD).next_to(rule, DOWN, buff=0.2).align_to(rule, RIGHT)
        self.play(FadeIn(eta_note, shift=DOWN * 0.15), Indicate(rule[0][4], color=GOOD), run_time=0.8)

        # 代入数字：同一行公式，字母一个个飞下来变成数——跟着念到哪个数，飞哪个
        lr = 1.5
        w0, s0 = -2.8, dL(-2.8)
        w1 = w0 - lr * s0
        nums = MathTex(r"-2.80", r"\;-\;", r"1.5", r"\times", r"(" + f"{s0:+.2f}" + r")", r"\;=\;", f"{w1:.2f}", font_size=60)
        nums[2].set_color(GOOD)
        nums[4].set_color(TAN)
        nums[6].set_color(DOT)
        nums.next_to(rule, DOWN, buff=0.6).align_to(rule, RIGHT)
        self.until(B["plug0"])
        self.play(FadeOut(eta_note, run_time=0.3), TransformFromCopy(rule[0][2], nums[0]), run_time=0.7)
        self.until(B["plug1"])
        self.play(TransformFromCopy(rule[0][3], nums[1]), TransformFromCopy(rule[0][4], nums[2]), run_time=0.7)
        self.until(B["plug2"])
        self.play(TransformFromCopy(rule[0][5], nums[3]), TransformFromCopy(rule[1], nums[4]), run_time=0.8)
        self.until(B["plug3"])
        self.play(Write(nums[5]), run_time=0.3)
        self.play(FadeIn(nums[6], scale=1.4), run_time=0.4)
        self.play(Circumscribe(nums[6], color=DOT, buff=0.1), run_time=0.7)

        # 走一步：先在横轴上画出这一步，点再沿着曲线滑过去
        self.until(B["step"])
        a0, a1 = ax.c2p(w0, 0), ax.c2p(w1, 0)
        stepv = Arrow(a0 + UP * 0.25, a1 + UP * 0.25, buff=0, color=DOT, stroke_width=6, max_tip_length_to_length_ratio=0.25)
        self.play(GrowArrow(stepv), run_time=0.4)
        ghost = Dot(dot.get_center(), radius=0.08, color=DOT, fill_opacity=0.35)
        self.add(ghost)
        self.play(w.animate.set_value(w1), run_time=0.8, rate_func=smooth)

        # 一步一步重复：步子随着坡变缓自己变小。右上角记步数和误差
        self.until(B["loop"])
        counter = VGroup(zh("第", 32, MUTED), Integer(1, font_size=48, color=DOT), zh("步", 32, MUTED)).arrange(RIGHT, buff=0.12)
        err = VGroup(zh("误差", 32, MUTED), DecimalNumber(L(w1), num_decimal_places=3, font_size=48, color=CURVE)).arrange(RIGHT, buff=0.2)
        hud = VGroup(counter, err).arrange(DOWN, aligned_edge=LEFT, buff=0.2).to_corner(UR, buff=0.6)
        self.play(FadeOut(nums), FadeOut(rule), FadeOut(slope_lbl), FadeOut(stepv), FadeIn(hud, shift=LEFT * 0.2), run_time=0.5)
        ws = descend(w1, lr, 6)
        ghosts = [ghost]
        for i, (a, b) in enumerate(zip(ws[:-1], ws[1:])):
            g = Dot(ax.input_to_graph_point(a, graph), radius=0.08, color=DOT, fill_opacity=0.35)
            ghosts.append(g)
            self.add(g)
            self.play(w.animate.set_value(b), counter[1].animate.set_value(i + 2), err[1].animate.set_value(L(b)),
                      run_time=max(0.3, 0.6 - i * 0.06), rate_func=smooth)

        # 镜头推近谷底：步子已经小得几乎看不见
        self.until(B["shrink"])
        cam = self.camera.frame
        cam.save_state()
        bottom = ax.input_to_graph_point(ws[-1], graph)
        self.play(cam.animate.scale(0.3).move_to(bottom + UP * 0.2), hud.animate.set_opacity(0), run_time=1.2)
        self.until(B["lr"] - 0.7)
        self.play(Restore(cam), run_time=0.6)

        # 步子迈多大，决定一切：同一条坡，三种 η
        self.until(B["lr"])
        self.remove(tan)
        self.play(FadeOut(VGroup(*ghosts)), FadeOut(dot), FadeOut(glow), FadeOut(hud), run_time=0.3)
        panels = VGroup(*[VGroup(ax.copy(), graph.copy()) for _ in range(3)])
        for p in panels:
            p.scale(0.41)
        panels.arrange(RIGHT, buff=0.4).move_to(DOWN * 0.35)
        self.play(ReplacementTransform(VGroup(ax, graph), panels[1]), FadeOut(xl), FadeOut(yl), run_time=0.8)
        head = zh("同一个坡，三种步子", 46, INK).to_edge(UP, buff=0.7)
        self.play(TransformFromCopy(panels[1], panels[0]), TransformFromCopy(panels[1], panels[2]), FadeIn(head, shift=DOWN * 0.2), run_time=0.7)

        parts = [head]
        cases = [("small", 0.15, 12, "太小：磨磨蹭蹭", MUTED), ("good", 1.5, 8, "刚好", GOOD), ("big", 4.0, 10, "太大：来回乱跳", TAN)]
        for k, (key, lr_, n, label, col) in enumerate(cases):
            self.until(B[key])
            pa = panels[k][0]
            eta = MathTex(r"\eta = " + f"{lr_:g}", font_size=52, color=col).next_to(pa, UP, buff=0.35)
            cap = zh(label, 38, col).next_to(pa, DOWN, buff=0.4)
            self.play(FadeIn(eta, shift=DOWN * 0.15), FadeIn(cap, shift=UP * 0.15), run_time=0.35)
            path = descend(-2.8, lr_, n)
            pts = [pa.c2p(np.clip(x, -3.4, 3.4), L(np.clip(x, -3.4, 3.4))) for x in path]
            d = Dot(pts[0], radius=0.08, color=DOT).set_z_index(5)
            self.add(d)
            segs = []
            per = 1.9 / len(pts)
            for p0, p1 in zip(pts[:-1], pts[1:]):
                seg = Line(p0, p1, color=DOT, stroke_width=3, stroke_opacity=0.7)
                segs.append(seg)
                self.play(d.animate.move_to(p1), Create(seg), *[x.animate.set_stroke(opacity=0.25) for x in segs[-4:-1]],
                          run_time=per, rate_func=linear)
            parts += [eta, cap, d, *segs]

        # 收回一条曲线，交给下一场：其实参数从来不止一个
        self.until(B["end2d"])
        one = VGroup(ax, graph)
        self.play(FadeOut(VGroup(*parts)), FadeOut(panels[0]), FadeOut(panels[2]), ReplacementTransform(panels[1], one), run_time=1.0)
        self.until(T3)


def L2(u, v):
    """两个参数的误差：横着是刚才那条曲线，竖着是一道窄沟"""
    return L(u) * 0.35 + 0.3 * (v + 0.2) ** 2


def grad2(u, v):
    return np.array([0.35 * dL(u), 0.6 * (v + 0.2)])


class Descent3D(Clocked, ThreeDScene):
    offset = T3

    def construct(self):
        ax = ThreeDAxes(x_range=[-3.5, 3.5, 1], y_range=[-2.5, 2.5, 1], z_range=[0, 3.2, 1], x_length=10.5, y_length=7.5, z_length=4.6,
                        axis_config=dict(color=MUTED, stroke_width=2, include_ticks=False, include_tip=False))
        ax.shift(DOWN * 1.2)
        # 开场接上一场：正面看过去，只有一条曲线（就是碗的一个切面）
        self.set_camera_orientation(phi=90 * DEGREES, theta=-90 * DEGREES, zoom=1.0)
        v0 = -0.2
        curve = ax.plot_parametric_curve(lambda t: np.array([t, v0, L2(t, v0)]), t_range=[-3.4, 3.4], color=CURVE, stroke_width=5)
        self.add(ax, curve)

        def bowl(s):
            # s=0 时整张曲面压在那条曲线上，s=1 是完整的碗：两张点数一样的曲面，变形时每个点各走各的
            surf = Surface(lambda u, v: ax.c2p(u, v0 + (v - v0) * s, L2(u, v0 + (v - v0) * s)), u_range=[-3.4, 3.4], v_range=[-2.4, 2.4],
                           resolution=(34, 24), fill_opacity=0.85, stroke_color=CURVE, stroke_width=0.5, stroke_opacity=0.3)
            surf.set_fill_by_value(axes=ax, colorscale=[("#0E2A40", 0.1), ("#1E6A8C", 0.9), (CURVE, 1.8), ("#9ADCF0", 3.0)], axis=2)
            return surf

        surf, flat = bowl(1), bowl(0.01)

        # 两个参数：镜头抬起来，曲面从这条线长出来
        self.until(B["lift"])
        self.move_camera(phi=64 * DEGREES, theta=-58 * DEGREES, zoom=0.7, frame_center=ax.c2p(-1.6, 0, 1.0),
                         added_anims=[Succession(FadeIn(flat, run_time=0.4), Transform(flat, surf, run_time=2.0))], run_time=2.4, rate_func=smooth)
        self.play(FadeOut(curve), run_time=0.3)

        # 两个旋钮一起拧：每一步都朝最陡的下坡方向
        self.until(B["path"])
        p = np.array([-2.9, 1.9])
        pts = [p.copy()]
        for _ in range(16):
            p = p - 2.9 * grad2(*p)
            pts.append(p.copy())
        pos = [ax.c2p(u, v, L2(u, v) + 0.06) for u, v in pts]
        ball = Sphere(radius=0.11, resolution=(10, 10)).set_color(DOT).move_to(pos[0])
        self.play(FadeIn(ball), run_time=0.3)
        self.begin_ambient_camera_rotation(rate=0.05)
        for i, (a, b) in enumerate(zip(pos[:-1], pos[1:])):
            seg = Line(a, b, color=DOT, stroke_width=4)
            self.play(ball.animate.move_to(b), Create(seg), run_time=max(0.15, 0.4 - i * 0.035), rate_func=smooth)

        l1 = zh("这里：2 个参数", 42, INK)
        l2 = zh("大模型：几十亿个", 42, DOT)
        l3 = zh("每一步，都往下坡挪一点", 34, MUTED)
        g = VGroup(VGroup(l1, l2).arrange(DOWN, aligned_edge=LEFT, buff=0.25), l3).arrange(DOWN, aligned_edge=LEFT, buff=0.45).to_corner(UL, buff=0.6)
        self.add_fixed_in_frame_mobjects(g)
        self.remove(g)
        for key, m in (("many", l1), ("many2", l2), ("many3", l3)):
            self.until(B[key])
            self.play(FadeIn(m, shift=UP * 0.2), run_time=0.5)
        self.until(B["end"])
