# -*- coding: utf-8 -*-
"""
三棱镜光的色散 · 交互式演示 (Pygame + 七色简化模型)

物理模型
--------
1. 斯涅尔定律: n1 * sin(theta1) = n2 * sin(theta2)
2. 简化色散: 玻璃对七种色光的折射率不同 ( violet 1.53 -> red 1.51 )
   —— 这就是"色散"的来源: 紫光偏折最多, 红光最少
3. 白光 = 七条彩色光线叠加的视觉效果
4. 棱镜内部发生第二次折射, 两次偏折同向叠加 -> 出射光展开成扇形

操作
----
  鼠标拖动   : 改变入射光方向 (也可拖动左上角的入射光手柄)
  滚轮 / ↑↓ : 微调入射角
  ← →       : 改变棱镜顶角 (30° ~ 90°)
  A D       : 旋转整个棱镜
  R         : 重置
  空格      : 暂停 / 继续 白光脉冲动画
  H         : 隐藏 / 显示辅助线(法线、入射点)
  数字 1-4  : 切换玻璃材质 (冕牌玻璃 / 火石玻璃 / 重火石 / 水)
  ESC       : 退出
"""

import math
import sys
import pygame

# ----------------------------------------------------------------------
# 基本设置
# ----------------------------------------------------------------------
WIDTH, HEIGHT = 1180, 760
FPS = 60

BG_COLOR = (16, 18, 28)
PANEL_COLOR = (26, 30, 45)
PRISM_FILL = (96, 110, 140)
PRISM_EDGE = (200, 215, 245)
PRISM_ALPHA_FILL = (60, 70, 100, 90)
TEXT_COLOR = (225, 230, 245)
SUB_COLOR = (150, 160, 185)
GRID_COLOR = (34, 39, 56)

# 七色光: 名称, 颜色(RGB), 折射率
SPECTRUM = [
    ("红 Red",     (255,  56,  56), 1.508),
    ("橙 Orange",  (255, 145,  40), 1.511),
    ("黄 Yellow",  (255, 226,  60), 1.514),
    ("绿 Green",   ( 56, 220, 105), 1.518),
    ("蓝 Blue",    ( 52, 145, 255), 1.523),
    ("靛 Indigo",  (110,  85, 240), 1.528),
    ("紫 Violet",  (200,  80, 255), 1.534),
]

# 不同玻璃材质: 名称 -> 折射率偏移量(整体缩放色散强度)
MATERIALS = [
    ("冕牌玻璃 (色散弱)", 1.00),
    ("火石玻璃 (色散强)", 2.15),
    ("重火石 (色散极强)", 3.30),
    ("水 (色散很弱)",     0.35),
]
BASE_N = 1.52          # 基准折射率
DISPERSION_SPREAD = 0.026   # 红-紫折射率差(基准)
AIR_N = 1.0

# ----------------------------------------------------------------------
# 几何工具
# ----------------------------------------------------------------------
def rotate(p, ang, origin):
    c, s = math.cos(ang), math.sin(ang)
    x, y = p[0] - origin[0], p[1] - origin[1]
    return (origin[0] + x * c - y * s, origin[1] + x * s + y * c)


def seg_intersect(p1, p2, p3, p4):
    """线段 p1p2 与 p3p4 的交点, 无交点返回 None"""
    x1, y1 = p1
    x2, y2 = p2
    x3, y3 = p3
    x4, y4 = p4
    den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
    if abs(den) < 1e-12:
        return None
    t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / den
    u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / den
    if 1e-9 <= t <= 1.0001 and -0.0001 <= u <= 1.0001:   # t>0: 排除起点所在边
        return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
    return None


def refract(d, n, eta):
    """
    折射方向。d 为单位入射向量, n 为单位法线(朝向入射侧), eta = n1/n2
    全反射返回 None
    """
    cosi = -max(-1.0, min(1.0, d[0] * n[0] + d[1] * n[1]))
    if cosi < 0:                       # 法线方向反了, 翻过来
        n = (-n[0], -n[1])
        cosi = -cosi
    sin2t = eta * eta * (1.0 - cosi * cosi)
    if sin2t > 1.0:
        return None                    # 全反射
    cost = math.sqrt(1.0 - sin2t)
    return (eta * d[0] + (eta * cosi - cost) * n[0],
            eta * d[1] + (eta * cosi - cost) * n[1])


def reflect(d, n):
    dot = d[0] * n[0] + d[1] * n[1]
    return (d[0] - 2 * dot * n[0], d[1] - 2 * dot * n[1])


def norm(v):
    m = math.hypot(*v) or 1e-9
    return (v[0] / m, v[1] / m)


# ----------------------------------------------------------------------
# 棱镜
# ----------------------------------------------------------------------
class Prism:
    def __init__(self, center, apex_deg=60, size=240):
        self.center = center
        self.apex_deg = apex_deg
        self.size = size
        self.rot = 0.0                 # 整体旋转(弧度)
        self.build()

    def build(self):
        """等腰三角形, 顶点朝右"""
        a = math.radians(self.apex_deg) / 2
        L = self.size
        top = (self.center[0] + L * math.cos(a) * 0.55, self.center[1] - L * math.sin(a))
        bot = (self.center[0] + L * math.cos(a) * 0.55, self.center[1] + L * math.sin(a))
        left = (self.center[0] - L * 0.62, self.center[1])
        self.apex = top
        self.verts = [left, top, bot]
        self.edges = [(left, top), (top, bot), (bot, left)]
        self.normals = []
        for p1, p2 in self.edges:
            e = (p2[0] - p1[0], p2[1] - p1[1])
            nrm = norm((-e[1], e[0]))
            mid = ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
            to_c = (self.center[0] - mid[0], self.center[1] - mid[1])
            if nrm[0] * to_c[0] + nrm[1] * to_c[1] < 0:
                nrm = (-nrm[0], -nrm[1])
            self.normals.append(nrm)

    def set_apex(self, deg):
        self.apex_deg = max(20.0, min(95.0, deg))
        self.build()

    def rotate(self, d):
        self.rot += d
        self.verts = [rotate(v, d, self.center) for v in self.verts]
        self.edges = [(self.verts[0], self.verts[1]),
                      (self.verts[1], self.verts[2]),
                      (self.verts[2], self.verts[0])]
        self.apex = self.verts[1]
        self.normals = []
        for p1, p2 in self.edges:
            e = (p2[0] - p1[0], p2[1] - p1[1])
            nrm = norm((-e[1], e[0]))
            mid = ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
            to_c = (self.center[0] - mid[0], self.center[1] - mid[1])
            if nrm[0] * to_c[0] + nrm[1] * to_c[1] < 0:
                nrm = (-nrm[0], -nrm[1])
            self.normals.append(nrm)


# ----------------------------------------------------------------------
# 光线追踪: 一条光穿过棱镜
# ----------------------------------------------------------------------
def trace_ray(origin, direction, prism, ior):
    """
    返回 (路径点列表, 是否全反射, 各段出射点法线信息)
    """
    pts = [origin]
    d = norm(direction)
    p = origin
    eta_in = AIR_N / ior
    eta_out = ior / AIR_N
    total_internal = False
    hit_info = []

    for bounce in range(6):
        best = None
        best_t = 1e18
        for idx, (p1, p2) in enumerate(prism.edges):
            ip = seg_intersect(p, (p[0] + d[0] * 4000, p[1] + d[1] * 4000), p1, p2)
            if ip:
                t = math.hypot(ip[0] - p[0], ip[1] - p[1])
                if t < best_t:
                    best_t = t
                    best = (ip, idx)
        if best is None:
            pts.append((p[0] + d[0] * 3000, p[1] + d[1] * 3000))
            break

        ip, idx = best
        pts.append(ip)
        n = prism.normals[idx]
        inside = (bounce % 2 == 1)
        eta = eta_out if inside else eta_in
        rd = refract(d, n, eta)
        if rd is None:
            total_internal = True
            rn = reflect(d, n)
            hit_info.append((ip, n, True))
            d = rn
            p = (ip[0] + d[0] * 1e-4, ip[1] + d[1] * 1e-4)
            continue
        hit_info.append((ip, n, False))
        # 沿法线朝折射侧偏移, 保证进入/离开介质而不是立即再碰到同一界面
        shift = 1e-3
        p = (ip[0] + n[0] * shift, ip[1] + n[1] * shift)
        d = rd
        if inside and bounce >= 1:
            pts.append((p[0] + d[0] * 3000, p[1] + d[1] * 3000))
            break

    return pts, total_internal, hit_info


# ----------------------------------------------------------------------
# 绘制辅助
# ----------------------------------------------------------------------
def draw_grid(surf):
    for x in range(0, WIDTH, 40):
        pygame.draw.line(surf, GRID_COLOR, (x, 0), (x, HEIGHT))
    for y in range(0, HEIGHT, 40):
        pygame.draw.line(surf, GRID_COLOR, (0, y), (WIDTH, y))


def draw_glow_line(surf, color, p1, p2, width=3, glow=12):
    """带辉光的光线"""
    if glow:
        for g in range(glow, 0, -3):
            a = surf.copy()
            a.set_alpha(28)
            pygame.draw.line(a, color, p1, p2, width + g)
            surf.blit(a, (0, 0))
    pygame.draw.line(surf, color, p1, p2, width)


def blend(c1, c2, t):
    return tuple(int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))


# ----------------------------------------------------------------------
# 主程序
# ----------------------------------------------------------------------
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("三棱镜 · 光的色散 Dispersion of Light  |  拖动鼠标改变入射角")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont("microsoftyahei,pingfangsc,wenquanyimicrohei,dejavusans", 20)
    small = pygame.font.SysFont("microsoftyahei,pingfangsc,wenquanyimicrohei,dejavusans", 15)
    title_font = pygame.font.SysFont("microsoftyahei,pingfangsc,wenquanyimicrohei,dejavusans", 26, bold=True)

    prism = Prism((640, 400), apex_deg=60, size=250)
    incident_deg = -8.0         # 入射光相对水平方向的角度(略向上, 打在迎光面上)
    mat_idx = 0
    show_help_lines = True
    dragging = False
    paused = False
    pulse = 0.0                 # 白光脉冲相位

    def light_origin():
        return (110.0, 400.0)

    def light_dir():
        a = math.radians(incident_deg)
        return (math.cos(a), math.sin(a))

    running = True
    while running:
        dt = clock.tick(FPS) / 1000.0
        if not paused:
            pulse = (pulse + dt * 0.55) % 1.0

        for ev in pygame.event.get():
            if ev.type == pygame.QUIT:
                running = False
            elif ev.type == pygame.KEYDOWN:
                if ev.key == pygame.K_ESCAPE:
                    running = False
                elif ev.key == pygame.K_r:
                    prism = Prism((640, 400), 60, 250)
                    incident_deg = -8.0
                    mat_idx = 0
                elif ev.key == pygame.K_SPACE:
                    paused = not paused
                elif ev.key == pygame.K_h:
                    show_help_lines = not show_help_lines
                elif ev.key == pygame.K_UP:
                    incident_deg = max(-70.0, incident_deg - 2)
                elif ev.key == pygame.K_DOWN:
                    incident_deg = min(70.0, incident_deg + 2)
                elif ev.key == pygame.K_LEFT:
                    prism.set_apex(prism.apex_deg - 3)
                elif ev.key == pygame.K_RIGHT:
                    prism.set_apex(prism.apex_deg + 3)
                elif ev.key == pygame.K_a:
                    prism.rotate(-math.radians(3))
                elif ev.key == pygame.K_d:
                    prism.rotate(math.radians(3))
                elif ev.key in (pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4):
                    mat_idx = ev.key - pygame.K_1
            elif ev.type == pygame.MOUSEWHEEL:
                incident_deg = max(-70.0, min(70.0, incident_deg - ev.y * 1.5))
            elif ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
                mx, my = ev.pos
                if my > HEIGHT - 200:      # 点在说明栏上不改变入射角
                    continue
                o = light_origin()
                # 点手柄附近或直接点画面上半部分 => 拖动入射方向
                if math.hypot(mx - o[0], my - o[1]) < 90 or my < HEIGHT - 210:
                    dragging = True
            elif ev.type == pygame.MOUSEBUTTONUP and ev.button == 1:
                dragging = False
            elif ev.type == pygame.MOUSEMOTION and dragging:
                mx, my = ev.pos
                if my > HEIGHT - 200:      # 点在说明栏上不改变入射角
                    continue
                o = light_origin()
                incident_deg = math.degrees(math.atan2(my - o[1], mx - o[0]))

        # ---------------- 绘制 ----------------
        screen.fill(BG_COLOR)
        draw_grid(screen)

        # 棱镜
        poly = prism.verts
        pygame.draw.polygon(screen, PRISM_FILL, poly)
        pygame.draw.polygon(screen, PRISM_EDGE, poly, 2)
        # 玻璃高光
        hl = screen.copy()
        hl.set_alpha(40)
        pygame.draw.polygon(hl, (255, 255, 255),
                            [prism.verts[0], prism.verts[1],
                             ((prism.verts[1][0] + prism.verts[2][0]) / 2,
                              (prism.verts[1][1] + prism.verts[2][1]) / 2)])
        screen.blit(hl, (0, 0))

        origin = light_origin()
        d = light_dir()
        endpoint = (origin[0] + d[0] * 260, origin[1] + d[1] * 260)

        # 入射白光
        draw_glow_line(screen, (255, 255, 255), origin, endpoint, 5, 16)
        # 脉冲光点
        if not paused:
            px = origin[0] + d[0] * 260 * pulse
            py = origin[1] + d[1] * 260 * pulse
            pygame.draw.circle(screen, (255, 255, 255), (int(px), int(py)), 6)

        strength = MATERIALS[mat_idx][1]
        rays = []
        for name, col, base_ior in SPECTRUM:
            ior = AIR_N + (BASE_N + (base_ior - BASE_N) * strength - AIR_N)
            pts, tir, info = trace_ray(endpoint, d, prism, ior)
            rays.append((name, col, pts, tir, info))

        # 棱镜内部: 各色光轻微分离
        for name, col, pts, tir, info in rays:
            if len(pts) >= 3:
                for i in range(1, len(pts) - 1):
                    draw_glow_line(screen, col, pts[i], pts[i + 1], 2, 6)

        # 出射扇形光谱
        exit_segs = []
        for name, col, pts, tir, info in rays:
            if len(pts) >= 3:
                last = pts[-1]
                prev = pts[-2]
                exit_segs.append((col, prev, last, name))
        for col, prev, last, name in exit_segs:
            draw_glow_line(screen, col, prev, last, 3, 14)
            if not paused:
                t = pulse
                gx = prev[0] + (last[0] - prev[0]) * t
                gy = prev[1] + (last[1] - prev[1]) * t
                pygame.draw.circle(screen, col, (int(gx), int(gy)), 5)

        # 法线与入射点
        if show_help_lines and rays:
            for _, _, pts, tir, info in rays[:1]:
                for ip, n, is_tir in info:
                    L = 42
                    pygame.draw.line(screen, (120, 200, 255),
                                     (ip[0] - n[0] * L, ip[1] - n[1] * L),
                                     (ip[0] + n[0] * L, ip[1] + n[1] * L), 1)
                    pygame.draw.circle(screen, (120, 200, 255), (int(ip[0]), int(ip[1])), 4, 1)

        # 屏幕上的光谱投影条
        proj_y = HEIGHT - 150
        pygame.draw.line(screen, (60, 66, 90), (60, proj_y), (WIDTH - 60, proj_y), 1)
        for col, prev, last, name in exit_segs:
            if last[0] > WIDTH or last[1] < 0 or last[1] > HEIGHT:
                continue
            x = max(60, min(WIDTH - 60, last[0]))
            pygame.draw.line(screen, col, (x, proj_y - 14), (x, proj_y + 14), 3)

        # ---------------- UI ----------------
        # 标题
        screen.blit(title_font.render("三棱镜 · 光的色散", True, TEXT_COLOR), (24, 18))
        screen.blit(small.render("Dispersion of Light — 白光由多种色光组成，玻璃对不同色光折射率不同，"
                                 "紫光偏折最大、红光最小，于是白光被展开成光谱。",
                                 True, SUB_COLOR), (26, 50))

        # 左侧数据面板
        px0, py0 = 20, 92
        pw, ph = 250, 196
        s = pygame.Surface((pw, ph), pygame.SRCALPHA)
        s.fill((20, 24, 38, 190))
        screen.blit(s, (px0, py0))
        pygame.draw.rect(screen, (70, 80, 110), (px0, py0, pw, ph), 1, border_radius=6)
        y = py0 + 12
        screen.blit(font.render("实时参数", True, TEXT_COLOR), (px0 + 14, y))
        y += 32
        screen.blit(small.render(f"入射角 (相对水平)  {incident_deg:6.1f}°", True, SUB_COLOR), (px0 + 14, y)); y += 24
        screen.blit(small.render(f"棱镜顶角          {prism.apex_deg:6.1f}°", True, SUB_COLOR), (px0 + 14, y)); y += 24
        screen.blit(small.render(f"玻璃整体旋转      {math.degrees(prism.rot)%360:6.1f}°", True, SUB_COLOR), (px0 + 14, y)); y += 24
        screen.blit(small.render(f"材质              {MATERIALS[mat_idx][0]}", True, SUB_COLOR), (px0 + 14, y)); y += 24
        n_red = AIR_N + (BASE_N + (SPECTRUM[0][2] - BASE_N) * strength - AIR_N)
        n_vio = AIR_N + (BASE_N + (SPECTRUM[-1][2] - BASE_N) * strength - AIR_N)
        screen.blit(small.render(f"n(红) = {n_red:.3f}   n(紫) = {n_vio:.3f}", True, SUB_COLOR), (px0 + 14, y)); y += 24
        screen.blit(small.render(f"Δn = {n_vio - n_red:.4f}  (色散强度)", True, (255, 210, 120)), (px0 + 14, y))

        # 右侧光谱图例
        lx, ly = WIDTH - 232, 92
        lw, lh = 212, 250
        s2 = pygame.Surface((lw, lh), pygame.SRCALPHA)
        s2.fill((20, 24, 38, 190))
        screen.blit(s2, (lx, ly))
        pygame.draw.rect(screen, (70, 80, 110), (lx, ly, lw, lh), 1, border_radius=6)
        y = ly + 12
        screen.blit(font.render("七色光折射率", True, TEXT_COLOR), (lx + 14, y))
        y += 34
        for name, col, base_ior in SPECTRUM:
            n_i = AIR_N + (BASE_N + (base_ior - BASE_N) * strength - AIR_N)
            pygame.draw.rect(screen, col, (lx + 14, y + 3, 16, 12), border_radius=3)
            screen.blit(small.render(f"{name:<11s} n = {n_i:.3f}", True, SUB_COLOR), (lx + 38, y))
            y += 26
        y += 4
        screen.blit(small.render("偏折角: 红 < 橙 < 黄 < 绿 < 蓝 < 靛 < 紫", True, (255, 210, 120)), (lx + 14, y))

        # 未击中棱镜时给出提示
        if not exit_segs:
            tip = font.render("光线没有打在棱镜的迎光面上 —— 请向上拖动或按 ↓ 调整入射角", True, (255, 190, 120))
            screen.blit(tip, (WIDTH // 2 - tip.get_width() // 2, HEIGHT - 200))

        # 底部操作说明
        help_txt = ("拖动鼠标 = 改变入射角   |   滚轮/↑↓ = 微调   |   ←→ = 顶角   |   A/D = 旋转棱镜   |   "
                    "1-4 = 切换材质   |   空格 = 暂停   |   H = 法线   |   R = 重置   |   ESC = 退出")
        screen.blit(small.render(help_txt, True, (130, 140, 168)), (24, HEIGHT - 34))

        # 入射光手柄
        pygame.draw.circle(screen, (255, 255, 255), (int(origin[0]), int(origin[1])), 9, 2)
        pygame.draw.circle(screen, (90, 100, 130), (int(origin[0]), int(origin[1])), 4)

        pygame.display.flip()

    pygame.quit()
    sys.exit(0)


if __name__ == "__main__":
    main()
