#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
华腾第一期培训 · 热场视频帧渲染
逐帧渲染 1920x1080 @30fps，输出 PNG 序列给 ffmpeg 合成。
"""
import os, math, shutil
from PIL import Image, ImageDraw, ImageFont, ImageChops, ImageFilter

W, H, FPS = 1920, 1080, 30
OUT = os.path.dirname(os.path.abspath(__file__))
FRM = os.path.join(OUT, "frames")

BOLD = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
REG  = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
IDX  = 2  # Noto Sans CJK SC

# ---- CI 色 ----
NAVY_D = (0, 18, 38)
NAVY   = (0, 36, 63)
BLUE   = (0, 56, 118)     # 官方深蓝
ORANGE = (221, 106, 10)   # 官方橙
CYAN   = (0, 200, 255)
WHITE  = (255, 255, 255)
DIM    = (150, 172, 200)
DIM2   = (104, 128, 158)

_fc = {}
def F(size, bold=True):
    k = (size, bold)
    if k not in _fc:
        _fc[k] = ImageFont.truetype(BOLD if bold else REG, size, index=IDX)
    return _fc[k]


# ============ 背景 ============
def make_bg():
    """竖向渐变 + 细网格"""
    bg = Image.new("RGB", (W, H))
    d = ImageDraw.Draw(bg)
    for y in range(H):
        t = y / (H - 1)
        e = t * t * (3 - 2 * t)
        c = tuple(round(NAVY_D[i] + (NAVY[i] - NAVY_D[i]) * e) for i in range(3))
        d.line([(0, y), (W, y)], fill=c)
    g = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    gd = ImageDraw.Draw(g)
    for x in range(0, W, 120):
        gd.line([(x, 0), (x, H)], fill=(120, 170, 220, 12))
    for y in range(0, H, 120):
        gd.line([(0, y), (W, y)], fill=(120, 170, 220, 12))
    return Image.alpha_composite(bg.convert("RGBA"), g)


def blob(radius, color, peak):
    """预渲染径向光斑"""
    s = radius * 2
    im = Image.new("L", (s, s), 0)
    d = ImageDraw.Draw(im)
    steps = 46
    for i in range(steps, 0, -1):
        r = radius * i / steps
        a = int(peak * (1 - i / steps) ** 2.1)
        d.ellipse([radius - r, radius - r, radius + r, radius + r], fill=a)
    im = im.filter(ImageFilter.GaussianBlur(radius * 0.09))
    out = Image.new("RGBA", (s, s), color + (0,))
    out.putalpha(im)
    return out


# ============ 元素 ============
class El:
    """一个可动画的图层元素"""
    def __init__(self, layer, x, y, delay=0.0, dur=0.5, rise=22, anchor="lt"):
        self.layer, self.delay, self.dur, self.rise = layer, delay, dur, rise
        w, h = layer.size
        if anchor == "ct":   x -= w // 2
        elif anchor == "rt": x -= w
        self.x, self.y = int(x), int(y)
        self.merged = False


def txt_layer(lines, font, color, lh=1.5, spacing=0, shadow=False):
    """把多行文字渲染成紧凑 RGBA 图层"""
    if isinstance(lines, str):
        lines = [lines]
    probe = Image.new("RGBA", (10, 10))
    pd = ImageDraw.Draw(probe)
    ws, asc = [], []
    for ln in lines:
        bb = pd.textbbox((0, 0), ln, font=font)
        ws.append(bb[2] - bb[0] + bb[0] * 2)
        asc.append(bb)
    lineh = int(font.size * lh)
    pad = 12
    w = max(ws) + pad * 2 + 8
    h = lineh * len(lines) + pad * 2
    im = Image.new("RGBA", (w, h), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    for i, ln in enumerate(lines):
        y = pad + i * lineh
        if shadow:
            d.text((pad + 2, y + 3), ln, font=font, fill=(0, 10, 24, 130))
        d.text((pad, y), ln, font=font, fill=color)
    return im


def rich_layer(parts, size, lh=1.5, bold=True, shadow=False):  # noqa
    """一行内多色：parts = [(text, color), ...]"""
    f = F(size, bold)
    probe = ImageDraw.Draw(Image.new("RGBA", (10, 10)))
    tw = sum(probe.textlength(t, font=f) for t, _ in parts)
    pad = 12
    im = Image.new("RGBA", (int(tw) + pad * 2 + 8, int(size * lh) + pad * 2), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    x = pad
    for t, c in parts:
        if shadow:
            d.text((x + 2, pad + 3), t, font=f, fill=(0, 10, 24, 130))
        d.text((x, pad), t, font=f, fill=c)
        x += probe.textlength(t, font=f)
    return im


def pill(text, size=30, fg=CYAN, bg=(0, 200, 255, 26), bd=(0, 200, 255, 90)):
    f = F(size, True)
    probe = ImageDraw.Draw(Image.new("RGBA", (10, 10)))
    tw = probe.textlength(text, font=f)
    dot = 12
    padx, pady = 30, 17
    w = int(tw + padx * 2 + dot + 14)
    h = int(size + pady * 2)
    im = Image.new("RGBA", (w, h), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    d.rounded_rectangle([0, 0, w - 1, h - 1], radius=h // 2, fill=bg, outline=bd, width=2)
    cy = h // 2
    d.ellipse([padx - 2, cy - dot // 2, padx - 2 + dot, cy + dot // 2], fill=fg)
    d.text((padx + dot + 12, cy - size * 0.72), text, font=f, fill=fg)
    return im


def card(title, sub, w, h, accent=CYAN, hot=False, num=None):
    im = Image.new("RGBA", (w, h), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    if hot:
        fill = (0, 90, 190, 118); out = accent + (170,)
    else:
        fill = (255, 255, 255, 17); out = (255, 255, 255, 44)
    d.rounded_rectangle([0, 0, w - 1, h - 1], radius=22, fill=fill, outline=out, width=2)
    d.rounded_rectangle([0, 0, 6, h - 1], radius=3, fill=accent + (235,))
    x = 40
    y = 34
    if num:
        fn = F(26, True)
        d.ellipse([x, y + 2, x + 40, y + 42], fill=accent + (46,), outline=accent + (150,), width=2)
        tw = d.textlength(num, font=fn)
        d.text((x + 20 - tw / 2, y + 22 - 26 * 0.72), num, font=fn, fill=accent)
        x += 60
    ft = F(46, True)
    d.text((x, y - 4), title, font=ft, fill=WHITE if hot else (232, 240, 250))
    fs = F(28, False)
    yy = y + 68
    for ln in sub:
        d.text((40, yy), ln, font=fs, fill=DIM if not hot else (196, 224, 255))
        yy += 42
    return im


# ============ 场景 ============
LOGO = Image.open(os.path.join(os.path.dirname(OUT), "huatech-logo.png")).convert("RGBA")
LOGO = LOGO.crop(LOGO.getbbox())
def logo_white(h):
    im = LOGO.resize((round(LOGO.width * h / LOGO.height), h), Image.LANCZOS)
    px = im.load()
    for y in range(im.height):
        for x in range(im.width):
            r, g, b, a = px[x, y]
            if a: px[x, y] = (255, 255, 255, a)
    return im

LG_BIG = logo_white(104)
LG_SM  = logo_white(48)

CX = W // 2
scenes = []

def S(dur, els):
    scenes.append({"dur": dur, "els": els})

# --- 1 开场 ---
S(2.9, [
    El(LG_BIG, CX, 400, 0.10, 0.75, 26, "ct"),
    El(txt_layer("数字员工培养计划", F(38, False), DIM), CX, 545, 0.55, 0.6, 18, "ct"),
    El(txt_layer("第 一 期", F(30, True), ORANGE), CX, 620, 0.85, 0.5, 14, "ct"),
])

# --- 2 主标题 ---
S(3.6, [
    El(pill("今天下午 · 全员参加"), CX, 268, 0.05, 0.5, 18, "ct"),
    El(rich_layer([("华腾养虾记", WHITE), ("（一）", ORANGE)], 158, 1.22, True, shadow=True),
       CX, 362, 0.22, 0.7, 34, "ct"),
    El(rich_layer([("打造属于", DIM), ("你自己", ORANGE), ("的数字员工", DIM)], 56, 1.4, True), CX, 618, 0.62, 0.6, 22, "ct"),
    El(txt_layer("全员参加　|　无需提前准备", F(34, False), DIM2), CX, 748, 0.95, 0.55, 16, "ct"),
])

# --- 3 钩子 ---
S(3.3, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(txt_layer("先想一个问题", F(36, False), CYAN), CX, 300, 0.05, 0.5, 16, "ct"),
    El(txt_layer(["你每天干的活里，", "有多少是在干第二遍？"], F(104, True), WHITE, 1.35, shadow=True),
       CX, 400, 0.3, 0.7, 28, "ct"),
    El(txt_layer("同样的活不干第二遍 —— 这才是效率的来源", F(38, False), ORANGE), CX, 760, 0.95, 0.6, 18, "ct"),
])

# --- 4 三层区分 ---
CW, CH = 500, 268
S(4.2, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(pill("第 1 章"), 130, 210, 0.05, 0.45, 14),
    El(txt_layer("先分清三层东西", F(78, True), WHITE, 1.3, shadow=True), 130, 288, 0.15, 0.55, 22),
    El(card("大模型", ["会思考的大脑", "GPT　Claude　豆包"], CW, CH, DIM2, False, "1"), 160, 500, 0.5, 0.55, 26),
    El(card("AI 工具", ["会聊天的助手", "能问，不能干"], CW, CH, CYAN, False, "2"), 710, 500, 0.75, 0.55, 26),
    El(card("数字员工", ["能动手干活的同事", "有工具 · 有记忆 · 有权限"], CW, CH, ORANGE, True, "3"), 1260, 500, 1.0, 0.55, 26),
    El(txt_layer("三者缺一，就退化成聊天机器人", F(32, False), DIM2), CX, 830, 1.45, 0.5, 14, "ct"),
])

# --- 5 答案 vs 结果 ---
S(3.5, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(txt_layer("最关键的一句话", F(36, False), CYAN), CX, 288, 0.05, 0.5, 16, "ct"),
    El(rich_layer([("问答型 AI 给你", DIM), ("答案", DIM)], 82, 1.35, True), CX, 392, 0.28, 0.6, 24, "ct"),
    El(rich_layer([("数字员工给你", WHITE), ("结果", ORANGE)], 96, 1.35, True), CX, 520, 0.62, 0.65, 26, "ct"),
    El(txt_layer("前者告诉你「这个文件可以用 Python 解析」", F(34, False), DIM2), CX, 720, 1.0, 0.55, 16, "ct"),
    El(txt_layer("后者直接把 32MB 白皮书拆完，给你一份带出处的问题清单", F(34, False), (196, 224, 255)), CX, 778, 1.2, 0.55, 16, "ct"),
])

# --- 6 四类场景 ---
BW, BH = 830, 168
S(4.6, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(pill("现场演示"), 130, 200, 0.05, 0.45, 14),
    El(txt_layer("四类真能用的场景", F(76, True), WHITE, 1.3, shadow=True), 130, 276, 0.15, 0.55, 22),
    El(card("产品设计 · 文档分析", ["几十兆招标文件 → 结构问题、前后矛盾、缺失项"], BW, BH, CYAN, True, None), 130, 452, 0.45, 0.5, 24),
    El(card("材料整理", ["会议纪要 → 事项规范；需求 → 用例清单"], BW, BH, CYAN, False, None), 990, 452, 0.62, 0.5, 24),
    El(card("代码理解", ["没人敢碰的老模块 → 结构说明 + 改造方案"], BW, BH, CYAN, False, None), 130, 646, 0.79, 0.5, 24),
    El(card("跨校复配", ["A 校已配事项 + B 校差异 → B 校配置方案"], BW, BH, ORANGE, True, None), 990, 646, 0.96, 0.5, 24),
    El(txt_layer("共同点：做错了不出门，重跑一次就行", F(32, False), DIM2), CX, 866, 1.4, 0.5, 14, "ct"),
])

# --- 7 红线 ---
S(3.4, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(pill("第 2 章 · 先立规矩", 30, ORANGE, (221, 106, 10, 30), (221, 106, 10, 110)), CX, 262, 0.05, 0.5, 16, "ct"),
    El(txt_layer("数据红线", F(112, True), WHITE, 1.28, shadow=True), CX, 348, 0.22, 0.6, 28, "ct"),
    El(rich_layer([("客户数据 · 源码 · 投标文件", ORANGE)], 62, 1.35, True), CX, 542, 0.6, 0.6, 24, "ct"),
    El(txt_layer("不 进 外 部 工 具", F(66, True), WHITE, 1.35), CX, 646, 0.85, 0.6, 24, "ct"),
    El(txt_layer("L0 公开　·　L1 内部　·　L2 客户相关　·　L3 敏感　—　拿不准的，先问再用", F(32, False), DIM2), CX, 806, 1.2, 0.5, 16, "ct"),
])

# --- 8 核心章节 ---
S(3.8, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(txt_layer("本场唯一无法自学的部分", F(36, False), CYAN), CX, 262, 0.05, 0.5, 16, "ct"),
    El(txt_layer("给它做入职培训", F(122, True), WHITE, 1.28, shadow=True), CX, 342, 0.24, 0.65, 30, "ct"),
    El(rich_layer([("你不是在写配置，你在", DIM), ("带新人", ORANGE)], 58, 1.4, True), CX, 552, 0.62, 0.6, 22, "ct"),
    El(txt_layer("员工手册 · 性格与价值观 · 认识你这个人 · 工作日志 · 设备清单", F(34, False), (196, 224, 255)), CX, 686, 0.95, 0.55, 18, "ct"),
    El(txt_layer("写一次，以后每次都生效", F(34, False), DIM2), CX, 762, 1.2, 0.5, 14, "ct"),
])

# --- 9 带走什么 ---
S(3.2, [
    El(LG_SM, 130, 92, 0.0, 0.4, 10),
    El(txt_layer("散会时你手上会有", F(38, False), CYAN), CX, 322, 0.05, 0.5, 16, "ct"),
    El(txt_layer(["一份写着你自己名字、", "你自己规矩的配置文件"], F(90, True), WHITE, 1.36, shadow=True),
       CX, 404, 0.28, 0.68, 28, "ct"),
    El(txt_layer("这就是你的数字员工的第一天", F(40, False), ORANGE), CX, 748, 0.95, 0.55, 18, "ct"),
])

# --- 10 收尾 ---
S(4.2, [
    El(txt_layer("同样的活", F(118, True), DIM, 1.3, shadow=True), CX, 268, 0.08, 0.6, 26, "ct"),
    El(txt_layer("不干第二遍", F(150, True), WHITE, 1.28, shadow=True), CX, 418, 0.34, 0.7, 32, "ct"),
    El(txt_layer("今天下午 · 全员参加", F(46, True), ORANGE), CX, 686, 0.8, 0.6, 22, "ct"),
    El(txt_layer("接下来是实际操作环节，别眨眼……", F(32, False), DIM2), CX, 776, 1.1, 0.5, 16, "ct"),
    El(LG_SM, CX, 900, 1.35, 0.55, 18, "ct"),
])


# ============ 渲染 ============
def ease(t):
    return 1 - (1 - t) ** 3

def main():
    if os.path.isdir(FRM):
        shutil.rmtree(FRM)
    os.makedirs(FRM)

    bg0 = make_bg()
    b_blue = blob(760, (0, 110, 235), 128)
    b_orng = blob(520, (221, 106, 10), 60)
    b_cyan = blob(600, (0, 190, 255), 74)

    total_t = sum(s["dur"] for s in scenes)
    print("scenes=%d  total=%.1fs" % (len(scenes), total_t))

    n = 0
    elapsed = 0.0
    FADE = 0.34

    for si, sc in enumerate(scenes):
        dur = sc["dur"]
        nf = int(round(dur * FPS))
        settled = Image.new("RGBA", (W, H), (0, 0, 0, 0))

        for fi in range(nf):
            t = fi / FPS
            gt = elapsed + t                     # 全局时间
            ph = gt / total_t

            # --- 背景 + 漂移光斑 ---
            fr = bg0.copy()
            bx = CX + int(560 * math.cos(ph * 2 * math.pi * 0.75 + 0.6))
            by = 300 + int(200 * math.sin(ph * 2 * math.pi * 0.55))
            fr.alpha_composite(b_blue, (bx - 760, by - 760))
            ox = CX + int(700 * math.sin(ph * 2 * math.pi * 0.62 + 2.1))
            oy = 820 + int(150 * math.cos(ph * 2 * math.pi * 0.48))
            fr.alpha_composite(b_orng, (ox - 520, oy - 520))
            cx2 = 240 + int(300 * math.sin(ph * 2 * math.pi * 0.4 + 4.0))
            cy2 = 980 + int(120 * math.cos(ph * 2 * math.pi * 0.7))
            fr.alpha_composite(b_cyan, (cx2 - 600, cy2 - 600))

            # --- 场景整体透明度（自带交叉淡入淡出）---
            g = 1.0
            if t < FADE:            g = ease(t / FADE)
            elif t > dur - FADE:    g = ease(max(0.0, (dur - t) / FADE))

            # --- 元素：已到位的合并进 settled ---
            layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
            layer.alpha_composite(settled)
            for el in sc["els"]:
                lt = t - el.delay
                if lt <= 0:
                    continue
                p = min(1.0, lt / el.dur)
                if p >= 1.0:
                    if not el.merged:
                        settled.alpha_composite(el.layer, (el.x, el.y))
                        el.merged = True
                        layer.alpha_composite(el.layer, (el.x, el.y))
                    continue
                e = ease(p)
                dy = int(el.rise * (1 - e))
                a = el.layer.getchannel("A").point(lambda v, k=e: int(v * k))
                tmp = el.layer.copy(); tmp.putalpha(a)
                layer.alpha_composite(tmp, (el.x, el.y + dy))

            if g < 0.999:
                a = layer.getchannel("A").point(lambda v, k=g: int(v * k))
                layer.putalpha(a)
            fr.alpha_composite(layer)

            # --- 底部进度条 ---
            d = ImageDraw.Draw(fr)
            d.rectangle([0, H - 5, W, H], fill=(255, 255, 255, 26))
            d.rectangle([0, H - 5, int(W * ph), H], fill=ORANGE + (235,))

            fr.convert("RGB").save(os.path.join(FRM, "f%05d.png" % n), compress_level=1)
            n += 1

        elapsed += dur
        print("  scene %d/%d done (%d frames)" % (si + 1, len(scenes), nf))

    print("TOTAL_FRAMES=%d" % n)
    print("DURATION=%.2f" % (n / FPS))

if __name__ == "__main__":
    main()
