import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageDraw, ImageTk
import threading
import time
import math
import os
import pygame
import pygame.midi


class MusicAndPaintBoard:
    def __init__(self, root):
        self.root = root
        self.root.title("学习机 - 音乐谱曲 + 绘画谱曲 双模式")
        self.root.geometry("1500x900")

        # 通用参数
        self.current_mode = "music"
        self.canvas_width = 800
        self.canvas_height = 500
        self.play_speed = 200
        self.is_playing = False
        self.current_frame = 0
        self.frames = []
        self.commands = []
        self.enable_sound = True

        # 初始化 MIDI
        self.init_midi()

        # MIDI 音符编号 (C4=60)
        self.midi_notes = {
            '1': 60,  # C4 - do
            '2': 62,  # D4 - re
            '3': 64,  # E4 - mi
            '4': 65,  # F4 - fa
            '5': 67,  # G4 - sol
            '6': 69,  # A4 - la
            '7': 71,  # B4 - si
        }

        # 颜色映射
        self.note_colors = {
            '1': (255, 80, 80), '2': (255, 165, 80), '3': (255, 255, 80),
            '4': (80, 255, 80), '5': (80, 150, 255), '6': (128, 80, 255),
            '7': (255, 128, 255),
        }

        # 绘画模式参数
        self.pen_x = self.canvas_width // 2
        self.pen_y = self.canvas_height // 2
        self.pen_angle = 0
        self.pen_down = True
        self.pen_color = (0, 0, 0)
        self.pen_size = 3
        self.step_distance = 25

        self.setup_ui()
        self.init_background()
        self.load_demo()

    def init_midi(self):
        """初始化 MIDI"""
        try:
            pygame.init()
            pygame.midi.init()
            # 打开默认输出设备
            self.midi_output = pygame.midi.Output(0)
            self.midi_output.set_instrument(0)  # 钢琴音色
            print("MIDI 初始化成功")
        except Exception as e:
            print(f"MIDI 初始化失败: {e}")
            self.midi_output = None

    def play_midi_note(self, note_num, duration, octave_shift=0):
        """播放 MIDI 音符"""
        if not self.enable_sound or self.midi_output is None:
            return

        try:
            # 计算 MIDI 音符编号
            midi_note = self.midi_notes[str(note_num)]
            if octave_shift == 1:
                midi_note += 12  # 高八度
            elif octave_shift == -1:
                midi_note -= 12  # 低八度

            # 播放音符 (note_on, 音量100)
            self.midi_output.note_on(midi_note, 100)

            # 在单独线程中等待后关闭音符
            def stop_note():
                time.sleep(duration / 1000.0)
                self.midi_output.note_off(midi_note, 100)

            threading.Thread(target=stop_note, daemon=True).start()

        except Exception as e:
            print(f"MIDI 播放失败: {e}")

    def setup_ui(self):
        """设置界面"""
        # 模式切换栏
        mode_frame = ttk.Frame(self.root)
        mode_frame.pack(fill=tk.X, padx=10, pady=5)

        ttk.Label(mode_frame, text="选择模式:", font=("Arial", 12, "bold")).pack(side=tk.LEFT, padx=5)

        self.mode_var = tk.StringVar(value="music")
        ttk.Radiobutton(mode_frame, text="🎵 音乐谱曲 (MIDI简谱)",
                        variable=self.mode_var, value="music",
                        command=self.switch_mode).pack(side=tk.LEFT, padx=10)
        ttk.Radiobutton(mode_frame, text="🎨 绘画谱曲 (数字画谱)",
                        variable=self.mode_var, value="paint",
                        command=self.switch_mode).pack(side=tk.LEFT, padx=10)

        # 声音开关
        self.sound_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(mode_frame, text="🔊 MIDI 声音", variable=self.sound_var,
                        command=self.toggle_sound).pack(side=tk.LEFT, padx=20)

        # 测试声音按钮
        ttk.Button(mode_frame, text="🎵 测试声音", command=self.test_sound).pack(side=tk.LEFT, padx=5)

        # 主容器
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

        # 左侧:画布
        left_frame = ttk.LabelFrame(main_frame, text="输出区域", padding=10)
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.canvas = tk.Canvas(left_frame, width=self.canvas_width,
                                height=self.canvas_height, bg='white',
                                relief=tk.SUNKEN, borderwidth=2)
        self.canvas.pack()

        # 状态栏
        status_frame = ttk.Frame(left_frame)
        status_frame.pack(fill=tk.X, pady=5)
        self.status_label = ttk.Label(status_frame, text="就绪 | MIDI 已加载", font=("Arial", 10))
        self.status_label.pack(side=tk.LEFT)
        self.frame_label = ttk.Label(status_frame, text="")
        self.frame_label.pack(side=tk.RIGHT)

        # 预览区
        preview_frame = ttk.LabelFrame(left_frame, text="帧预览", padding=5)
        preview_frame.pack(fill=tk.X, pady=5)
        self.preview_canvas = tk.Canvas(preview_frame, height=100, bg='lightgray')
        self.preview_canvas.pack(fill=tk.X)

        # 右侧:编辑器
        right_frame = ttk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))

        # 乐谱/画谱编辑器
        editor_frame = ttk.LabelFrame(right_frame, text="简谱编辑器 (数字序列)", padding=10)
        editor_frame.pack(fill=tk.BOTH, expand=True)

        # 帮助信息
        self.help_text = tk.Text(editor_frame, height=8, font=("Courier", 9),
                                 bg='#f0f0f0', wrap=tk.WORD)
        self.help_text.pack(fill=tk.X, pady=5)

        # 输入区
        input_frame = ttk.Frame(editor_frame)
        input_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        ttk.Label(input_frame, text="简谱序列:").pack(anchor=tk.W)

        text_frame = ttk.Frame(input_frame)
        text_frame.pack(fill=tk.BOTH, expand=True)

        self.score_text = tk.Text(text_frame, height=10, font=("Courier", 12))
        self.score_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        score_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.score_text.yview)
        score_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.score_text.config(yscrollcommand=score_scrollbar.set)

        # 快速音符按钮
        notes_frame = ttk.LabelFrame(editor_frame, text="快速输入", padding=5)
        notes_frame.pack(fill=tk.X, pady=5)

        self.note_buttons_frame = ttk.Frame(notes_frame)
        self.note_buttons_frame.pack()
        self.update_quick_buttons()

        # 控制按钮
        control_frame = ttk.Frame(editor_frame)
        control_frame.pack(fill=tk.X, pady=5)

        ttk.Button(control_frame, text="▶ 播放", command=self.play).pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
        ttk.Button(control_frame, text="⏸ 暂停", command=self.pause).pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
        ttk.Button(control_frame, text="⏹ 停止", command=self.stop).pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
        ttk.Button(control_frame, text="💾 保存", command=self.save_score).pack(side=tk.LEFT, padx=2, expand=True,
                                                                               fill=tk.X)
        ttk.Button(control_frame, text="📁 加载", command=self.load_score).pack(side=tk.LEFT, padx=2, expand=True,
                                                                               fill=tk.X)
        ttk.Button(control_frame, text="📤 导出", command=self.export_file).pack(side=tk.LEFT, padx=2, expand=True,
                                                                                fill=tk.X)

        # 速度控制
        speed_frame = ttk.Frame(editor_frame)
        speed_frame.pack(fill=tk.X, pady=5)
        ttk.Label(speed_frame, text="播放速度:").pack(side=tk.LEFT)
        self.speed_var = tk.IntVar(value=120)
        speed_scale = ttk.Scale(speed_frame, from_=100, to=1000, variable=self.speed_var,
                                orient=tk.HORIZONTAL, length=200, command=self.update_speed)
        speed_scale.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
        self.speed_label = ttk.Label(speed_frame, text="120ms")
        self.speed_label.pack(side=tk.LEFT)

        # 命令列表
        list_frame = ttk.LabelFrame(right_frame, text="解析后的序列", padding=5)
        list_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        self.command_listbox = tk.Listbox(list_frame, height=6, font=("Courier", 10))
        self.command_listbox.pack(fill=tk.BOTH, expand=True)

        list_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.command_listbox.yview)
        list_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.command_listbox.config(yscrollcommand=list_scrollbar.set)

        self.update_help_text()

    def test_sound(self):
        """测试 MIDI 声音"""
        if self.midi_output:
            self.play_midi_note(1, 500, 0)
            self.status_label.config(text="测试声音: 播放 do 音")

    def toggle_sound(self):
        """切换声音"""
        self.enable_sound = self.sound_var.get()

    def update_quick_buttons(self):
        """更新快速按钮"""




        for widget in self.note_buttons_frame.winfo_children():
            widget.destroy()

        if self.mode_var.get() == "music":
            music_buttons = [
                ('1 do', '1'), ('2 re', '2'), ('3 mi', '3'), ('4 fa', '4'),
                ('5 sol', '5'), ('6 la', '6'), ('7 si', '7'),
                ('- 延长', '-'), ('. 低音', '.'), ("' 高音", "'"),
                ('| 小节', '|'), ('_ 八分', '_'), ('🎵 示例曲', 'demo_song'),  ('清空', 'clear')
            ]
            for i, (text, note) in enumerate(music_buttons):
                if note == 'clear':
                    btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
                                     command=self.clear_score)
                else:
                    btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
                                     command=lambda n=note: self.insert_note(n))
                btn.grid(row=i // 7, column=i % 7, padx=2, pady=2)
        else:
            paint_buttons = [
                ('1前进', '1'), ('2后退', '2'), ('3左转', '3'), ('4右转', '4'),
                ('5抬笔', '5'), ('6落笔', '6'), ('7颜色', '7'),
                ('8画圆', '8'), ('9画方', '9'), ('0清屏', '0'), ('.画点', '.'),
                ('红', '7 1'), ('绿', '7 2'), ('蓝', '7 3'), ('清空', 'clear'),
                ('左转45', '3 45'), ('右转45', '4 45')
            ]
            for i, (text, note) in enumerate(paint_buttons):
                if note == 'clear':
                    btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
                                     command=self.clear_score)
                else:
                    btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
                                     command=lambda n=note: self.insert_note(n))
                btn.grid(row=i // 9, column=i % 9, padx=2, pady=2)

    def update_help_text(self):
        """更新帮助文本"""
        self.help_text.delete("1.0", tk.END)

        if self.mode_var.get() == "music":
            help_text = """
╔══════════════════════════════════════════════════════════════════════════════╗
║                  🎵 音乐简谱编辑器 (MIDI 专业音色)                           ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  音符: 1=do 2=re 3=mi 4=fa 5=sol 6=la 7=si                                   ║
║  时值: - 延长一倍   _ 八分音符   数字后无符号=四分音符                        ║
║  音高: .低音  '高音  (如 1. 低音do, 1' 高音do)                               ║
║  使用 MIDI 钢琴音色播放,声音更专业!                                         ║
║  示例: 1 2 3 4 5 6 7 | 1- 2- 3- 4- | 1. 2. 3. 4. | 1' 2' 3' 4'              ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
        else:
            help_text = """
╔══════════════════════════════════════════════════════════════════════════════╗
║                         🎨 绘画简谱编辑器 (支持任意角度)                      ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  1=前进  2=后退  3=左转角度  4=右转角度  5=抬笔  6=落笔                      ║
║  7=颜色  8=画圆  9=画方  0=清屏  .=画点                                     ║
║                                                                              ║
║  角度用法:                                                                    ║
║    3 90     - 左转90度                                                       ║
║    3 45     - 左转45度                                                       ║
║    4 30     - 右转30度                                                       ║
║                                                                              ║
║  示例1(正方形): 1 1 1 1 3 90 1 1 1 1 3 90 1 1 1 1 3 90 1 1 1 1              ║
║  示例2(正六边形): 1 1 1 1 3 60 增加6次                                         ║
║  示例3:1 3 30 增加11次                                                       ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
        self.help_text.insert(tk.END, help_text)

    def switch_mode(self):
        """切换模式"""
        self.update_help_text()
        self.update_quick_buttons()
        self.init_background()
        self.load_demo()
        self.stop()

    def init_background(self):
        """初始化背景"""
        self.background = Image.new('RGB', (self.canvas_width, self.canvas_height), (255, 255, 255))
        draw = ImageDraw.Draw(self.background)

        if self.mode_var.get() == "music":
            staff_y = [150, 180, 210, 240, 270]
            for y in staff_y:
                draw.line((50, y, self.canvas_width - 50, y), fill=(0, 0, 0), width=1)
            draw.text((60, 155), "𝄞", fill=(100, 100, 100))
            for x in range(100, self.canvas_width - 50, 100):
                draw.line((x, 140, x, 280), fill=(0, 0, 0), width=1)
        else:
            step = 40
            for x in range(0, self.canvas_width, step):
                draw.line((x, 0, x, self.canvas_height), fill=(230, 230, 230), width=1)
            for y in range(0, self.canvas_height, step):
                draw.line((0, y, self.canvas_width, y), fill=(230, 230, 230), width=1)
            draw.ellipse((self.canvas_width // 2 - 5, self.canvas_height // 2 - 5,
                          self.canvas_width // 2 + 5, self.canvas_height // 2 + 5),
                         fill=(200, 200, 200))
            draw.line((0, self.canvas_height // 2, self.canvas_width, self.canvas_height // 2),
                      fill=(180, 180, 180), width=1)
            draw.line((self.canvas_width // 2, 0, self.canvas_width // 2, self.canvas_height),
                      fill=(180, 180, 180), width=1)

        self.image = self.background.copy()
        self.update_display()

    def load_demo(self):
        """加载演示内容"""
        if self.mode_var.get() == "music":
            demo = "553-5'5'3'-35513-3-1332126.-6.216.116.-"
        else:
            demo = "553-5'5'3'-35513-3-1332126.-6.216.116.-"
        self.score_text.delete("1.0", tk.END)
        self.score_text.insert(tk.END, demo)

    def insert_note(self, note):
        """插入音符/指令"""
        current = self.score_text.get("1.0", tk.END).strip()
        if note == 'clear':
            self.clear_score()
        elif note == 'demo_song':
            self.load_demo_song()
        else:
            new_text = current + " " + note if current else note
            self.score_text.delete("1.0", tk.END)
            self.score_text.insert(tk.END, new_text)

    def load_demo_song(self):
        """加载示例歌曲(送别变奏曲)"""
        demo = "5-351'--76-1'-5---5-123-212----5-351'--76-1'-5---5-234--7.1----"
        self.score_text.delete("1.0", tk.END)
        self.score_text.insert(tk.END, demo)
        self.status_label.config(text="已加载示例曲谱,点击播放")


    def clear_score(self):
        """清空输入"""
        self.score_text.delete("1.0", tk.END)

    def parse_music_score(self, text):
        """解析音乐简谱(支持无空格连续格式)"""
        notes = []

        # 先移除所有空格和换行
        text = text.strip().replace(' ', '').replace('\n', '').replace('\r', '')

        i = 0
        length = len(text)

        while i < length:
            char = text[i]

            if char == '|':
                notes.append(('bar', 0))
                i += 1
                continue

            # 解析音符 (1-7)
            if char in self.midi_notes:
                note_num = int(char)
                duration = 400  # 默认四分音符
                octave_shift = 0
                i += 1

                # 解析后续修饰符
                while i < length:
                    next_char = text[i]
                    if next_char == '-':
                        duration = 800  # 延长一倍
                        i += 1
                    elif next_char == '_':
                        duration = 200  # 八分音符
                        i += 1
                    elif next_char == '=':
                        duration = 100  # 十六分音符
                        i += 1
                    elif next_char == '.':
                        octave_shift = -1  # 低音
                        i += 1
                    elif next_char == "'":
                        octave_shift = 1  # 高音
                        i += 1
                    else:
                        break

                notes.append(('note', note_num, octave_shift, duration))

            # 跳过其他字符(如空格、换行等已经在开头处理了)
            else:
                i += 1

        return notes

    def parse_paint_score(self, text):
        """解析绘画简谱"""
        commands = []
        tokens = text.split()

        i = 0
        while i < len(tokens):
            token = tokens[i]

            if token == '1':
                commands.append(('forward', self.step_distance))
            elif token == '2':
                commands.append(('backward', self.step_distance))
            elif token == '3':
                angle = 90
                if i + 1 < len(tokens):
                    try:
                        angle = int(tokens[i + 1])
                        i += 1
                    except:
                        pass
                commands.append(('left', angle))
            elif token == '4':
                angle = 90
                if i + 1 < len(tokens):
                    try:
                        angle = int(tokens[i + 1])
                        i += 1
                    except:
                        pass
                commands.append(('right', angle))
            elif token == '5':
                commands.append(('pen_up',))
            elif token == '6':
                commands.append(('pen_down',))
            elif token == '7':
                if i + 1 < len(tokens):
                    color_num = tokens[i + 1]
                    color_map = {'1': (255, 0, 0), '2': (0, 255, 0), '3': (0, 0, 255),
                                 '4': (255, 255, 0), '5': (255, 0, 255), '6': (0, 255, 255)}
                    if color_num in color_map:
                        commands.append(('color', color_map[color_num]))
                        i += 1
            elif token == '8':
                if i + 1 < len(tokens):
                    try:
                        radius = int(tokens[i + 1])
                        commands.append(('circle', radius))
                        i += 1
                    except:
                        commands.append(('circle', 30))
            elif token == '9':
                if i + 1 < len(tokens):
                    try:
                        size = int(tokens[i + 1])
                        commands.append(('rect', size))
                        i += 1
                    except:
                        commands.append(('rect', 40))
            elif token == '0':
                commands.append(('clear',))
            elif token == '.':
                commands.append(('dot',))

            i += 1

        return commands

    def play(self):
        """播放"""
        text = self.score_text.get("1.0", tk.END).strip()
        if not text:
            messagebox.showwarning("警告", "请输入简谱内容!")
            return

        if self.mode_var.get() == "music":
            self.play_music(text)
        else:
            self.play_paint(text)

    def play_music(self, text):
        """播放音乐模式(支持无空格连续简谱)"""
        notes = self.parse_music_score(text)

        if not notes:
            messagebox.showwarning("警告", "无法解析乐谱!")
            return

        self.commands = notes
        self.frames = []
        self.note_timing = []
        current_img = self.background.copy()
        draw = ImageDraw.Draw(current_img)

        x_pos = 100
        frame_index = 0

        for note_info in notes:
            if note_info[0] == 'bar':
                draw.line((x_pos, 140, x_pos, 280), fill=(0, 0, 0), width=2)
                x_pos += 30
                for _ in range(2):
                    self.frames.append(current_img.copy())
                    frame_index += 1
                continue

            _, note_num, octave, duration = note_info

            # 计算Y位置
            base_y = 210
            y_pos = base_y - (note_num - 4) * 15 - octave * 20
            y_pos = max(50, min(self.canvas_height - 50, y_pos))

            # 画音符
            color = self.note_colors[str(note_num)]
            draw.ellipse((x_pos - 10, y_pos - 10, x_pos + 10, y_pos + 10), fill=color, outline=(0, 0, 0), width=1)
            draw.line((x_pos + 10, y_pos, x_pos + 10, y_pos - 40), fill=(0, 0, 0), width=2)

            # 添加音符文字
            octave_mark = ""
            if octave == 1:
                octave_mark = "'"
            elif octave == -1:
                octave_mark = "."
            draw.text((x_pos - 8, y_pos - 25), f"{note_num}{octave_mark}", fill=(0, 0, 0))

            # 时值标记(延长线)
            if duration >= 800:
                draw.line((x_pos + 15, y_pos - 20, x_pos + 45, y_pos - 20), fill=(0, 0, 0), width=2)
            elif duration <= 200:
                draw.line((x_pos + 15, y_pos - 20, x_pos + 25, y_pos - 20), fill=(0, 0, 0), width=1)

            # 关键修改:根据声音时长计算帧数,让动画和声音同步
            # 帧数 = 声音时长(ms) / 每帧时长(ms)
            frame_count = max(2, int(duration / self.play_speed))

            start_frame = frame_index
            for _ in range(frame_count):
                self.frames.append(current_img.copy())
                frame_index += 1

            # 存储音符信息
            self.note_timing.append({
                'start_frame': start_frame,
                'duration': duration,  # 使用原始时长
                'note_num': note_num,
                'octave': octave
            })

            # 移动位置,根据时值调整间距
            if duration >= 800:
                x_pos += 55
            elif duration <= 200:
                x_pos += 35
            else:
                x_pos += 45

            if x_pos > self.canvas_width - 60:
                x_pos = 60

        # 结束帧
        for _ in range(5):
            self.frames.append(current_img.copy())

        # 显示解析结果
        cmd_list = []
        for n in notes:
            if n[0] == 'note':
                mark = ""
                if n[2] == -1:
                    mark = "."
                elif n[2] == 1:
                    mark = "'"
                dur_str = ""
                if n[3] >= 800:
                    dur_str = "-"
                elif n[3] <= 200:
                    dur_str = "_"
                cmd_list.append(f"{n[1]}{mark}{dur_str}")
        self.update_command_list(cmd_list)

        self.start_playback_with_sound()
        self.status_label.config(text=f"音乐模式 - 解析 {len([n for n in notes if n[0] == 'note'])} 个音符")

    def start_playback_with_sound(self):
        """开始播放(带声音同步)"""
        if not self.frames:
            messagebox.showwarning("警告", "没有生成动画!")
            return

        self.current_frame = 0
        self.is_playing = True
        self.sound_triggered = set()  # 记录已触发的声音
        self.update_display()
        threading.Thread(target=self._play_with_sound, daemon=True).start()

    def _play_with_sound(self):
        """带声音同步的播放线程"""
        while self.is_playing and self.current_frame < len(self.frames):
            # 检查是否需要触发音符声音
            for note_info in self.note_timing:
                frame_idx = note_info['start_frame']
                if frame_idx not in self.sound_triggered and self.current_frame >= frame_idx:
                    # 到达该音符的起始帧,播放声音
                    self.sound_triggered.add(frame_idx)
                    self.play_midi_note(
                        note_info['note_num'],
                        note_info['duration'],
                        note_info['octave']
                    )

            time.sleep(self.play_speed / 1000.0)
            self.current_frame += 1
            self.root.after(0, self.update_display)
            self.root.after(0, self.update_preview)


    def play_paint(self, text):
        """播放绘画模式"""
        commands = self.parse_paint_score(text)

        if not commands:
            messagebox.showwarning("警告", "无法解析绘画指令!")
            return

        self.commands = commands

        self.pen_x = self.canvas_width // 2
        self.pen_y = self.canvas_height // 2
        self.pen_angle = 0
        self.pen_down = True
        self.pen_color = (0, 0, 0)

        self.frames = []
        current_img = self.background.copy()
        draw = ImageDraw.Draw(current_img)

        for cmd in commands:
            if cmd[0] == 'forward':
                step = cmd[1]
                rad = math.radians(self.pen_angle)
                new_x = self.pen_x + step * math.cos(rad)
                new_y = self.pen_y + step * math.sin(rad)
                if self.pen_down:
                    draw.line((self.pen_x, self.pen_y, new_x, new_y),
                              fill=self.pen_color, width=self.pen_size)
                self.pen_x, self.pen_y = new_x, new_y
                for _ in range(10):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'backward':
                step = cmd[1]
                rad = math.radians(self.pen_angle)
                new_x = self.pen_x - step * math.cos(rad)
                new_y = self.pen_y - step * math.sin(rad)
                if self.pen_down:
                    draw.line((self.pen_x, self.pen_y, new_x, new_y),
                              fill=self.pen_color, width=self.pen_size)
                self.pen_x, self.pen_y = new_x, new_y
                for _ in range(10):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'left':
                self.pen_angle -= cmd[1]
                for _ in range(5):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'right':
                self.pen_angle += cmd[1]
                for _ in range(5):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'pen_up':
                self.pen_down = False
                for _ in range(5):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'pen_down':
                self.pen_down = True
                for _ in range(5):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'color':
                self.pen_color = cmd[1]
                for _ in range(5):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'circle':
                radius = cmd[1]
                draw.ellipse((self.pen_x - radius, self.pen_y - radius,
                              self.pen_x + radius, self.pen_y + radius),
                             outline=self.pen_color, width=self.pen_size)
                for _ in range(15):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'rect':
                size = cmd[1]
                draw.rectangle((self.pen_x - size, self.pen_y - size,
                                self.pen_x + size, self.pen_y + size),
                               outline=self.pen_color, width=self.pen_size)
                for _ in range(15):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'clear':
                current_img = self.background.copy()
                draw = ImageDraw.Draw(current_img)
                for _ in range(5):
                    self.frames.append(current_img.copy())

            elif cmd[0] == 'dot':
                draw.ellipse((self.pen_x - 4, self.pen_y - 4,
                              self.pen_x + 4, self.pen_y + 4),
                             fill=self.pen_color)
                for _ in range(10):
                    self.frames.append(current_img.copy())

        cmd_names = {
            'forward': '前进', 'backward': '后退', 'left': '左转', 'right': '右转',
            'pen_up': '抬笔', 'pen_down': '落笔', 'color': '换色', 'circle': '画圆',
            'rect': '画方', 'clear': '清屏', 'dot': '画点'
        }

        display_cmds = []
        for c in commands:
            if c[0] == 'left':
                display_cmds.append(f'左转{c[1]}°')
            elif c[0] == 'right':
                display_cmds.append(f'右转{c[1]}°')
            else:
                display_cmds.append(cmd_names.get(c[0], c[0]))
        self.update_command_list(display_cmds)

        self.start_playback()
        self.status_label.config(text=f"绘画模式 - 已生成 {len(self.frames)} 帧")

    def update_command_list(self, commands):
        """更新命令列表"""
        self.command_listbox.delete(0, tk.END)
        for i, cmd in enumerate(commands[:30]):
            self.command_listbox.insert(tk.END, f"{i + 1}: {cmd}")

    def start_playback(self):
        """开始播放"""
        if not self.frames:
            messagebox.showwarning("警告", "没有生成动画!")
            return

        self.current_frame = 0
        self.is_playing = True
        self.update_display()
        threading.Thread(target=self._play, daemon=True).start()

    def _play(self):
        while self.is_playing and self.current_frame < len(self.frames):
            time.sleep(self.play_speed / 1000.0)
            self.current_frame += 1
            self.root.after(0, self.update_display)
            self.root.after(0, self.update_preview)

    def update_display(self):
        """更新显示"""
        if self.current_frame < len(self.frames):
            img = self.frames[self.current_frame]
            img_resized = img.resize((self.canvas_width, self.canvas_height), Image.Resampling.LANCZOS)
            img_tk = ImageTk.PhotoImage(img_resized)
            self.canvas.delete("all")
            self.canvas.create_image(0, 0, anchor=tk.NW, image=img_tk)
            self.canvas.image = img_tk
            self.frame_label.config(text=f"帧: {self.current_frame + 1}/{len(self.frames)}")

    def update_preview(self):
        """更新预览"""
        self.preview_canvas.delete("all")
        if not self.frames:
            return

        preview_height = 80
        preview_width = int(preview_height * self.canvas_width / self.canvas_height)
        cols = min(10, len(self.frames))

        for i in range(cols):
            x = i * (preview_width + 5) + 5
            frame_idx = int(i * len(self.frames) / cols)
            if frame_idx < len(self.frames):
                img_small = self.frames[frame_idx].resize((preview_width, preview_height), Image.Resampling.LANCZOS)
                img_tk = ImageTk.PhotoImage(img_small)
                self.preview_canvas.create_image(x, 10, anchor=tk.NW, image=img_tk)
                self.preview_canvas.image = img_tk

    def pause(self):
        self.is_playing = False
        self.status_label.config(text="已暂停")

    def stop(self):
        self.is_playing = False
        self.current_frame = 0
        self.update_display()
        self.status_label.config(text="已停止")

    def update_speed(self, *args):
        self.play_speed = self.speed_var.get()
        self.speed_label.config(text=f"{self.play_speed}ms")

    def save_score(self):
        """保存乐谱/画谱"""
        text = self.score_text.get("1.0", tk.END).strip()
        if not text:
            messagebox.showwarning("警告", "没有内容可保存!")
            return

        ext = ".music" if self.mode_var.get() == "music" else ".paint"
        filename = filedialog.asksaveasfilename(defaultextension=ext,
                                                filetypes=[("简谱文件", f"*{ext}"), ("文本文件", "*.txt")])
        if filename:
            try:
                with open(filename, 'w', encoding='utf-8') as f:
                    f.write(text)
                messagebox.showinfo("成功", f"已保存到: {filename}")
            except Exception as e:
                messagebox.showerror("错误", f"保存失败: {str(e)}")

    def load_score(self):
        """加载乐谱/画谱"""
        ext = ".music" if self.mode_var.get() == "music" else ".paint"
        filename = filedialog.askopenfilename(filetypes=[("简谱文件", f"*{ext}"), ("文本文件", "*.txt")])
        if filename:
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    text = f.read()
                self.score_text.delete("1.0", tk.END)
                self.score_text.insert(tk.END, text)
                messagebox.showinfo("成功", "已加载文件")
            except Exception as e:
                messagebox.showerror("错误", f"加载失败: {str(e)}")

    def export_file(self):
        """导出文件"""
        if self.mode_var.get() == "music":
            self.export_text()
        else:
            self.export_gif()

    def export_text(self):
        """导出文本格式"""
        text = self.score_text.get("1.0", tk.END).strip()
        filename = filedialog.asksaveasfilename(defaultextension=".txt",
                                                filetypes=[("文本文件", "*.txt")])
        if filename:
            try:
                with open(filename, 'w', encoding='utf-8') as f:
                    f.write(f"=== 音乐简谱 ===\n")
                    f.write(f"速度: {self.play_speed}ms/拍\n")
                    f.write(f"序列:\n{text}\n")
                messagebox.showinfo("成功", "已导出文件")
            except Exception as e:
                messagebox.showerror("错误", f"导出失败: {str(e)}")

    def export_gif(self):
        """导出GIF"""
        if not self.frames:
            messagebox.showwarning("警告", "没有动画可导出!请先播放生成动画")
            return

        filename = filedialog.asksaveasfilename(defaultextension=".gif",
                                                filetypes=[("GIF动画", "*.gif")])
        if filename:
            try:
                self.frames[0].save(filename, save_all=True,
                                    append_images=self.frames[1:],
                                    duration=self.play_speed, loop=0)
                messagebox.showinfo("成功", f"GIF已保存!")
            except Exception as e:
                messagebox.showerror("错误", f"导出失败: {str(e)}")

    def __del__(self):
        """清理 MIDI 资源"""
        try:
            if hasattr(self, 'midi_output') and self.midi_output:
                self.midi_output.close()
            pygame.midi.quit()
            pygame.quit()
        except:
            pass


if __name__ == "__main__":
    root = tk.Tk()
    app = MusicAndPaintBoard(root)
    root.mainloop()

更多推荐