用OpenCV实现烟花动画

Python中可以使用PyGame实现动画,这里抛弃PyGame,使用OpenCV(cv2)实现绘图

python 复制代码
import random
import time
import cv2
import numpy as np
import local
import math

# 自定义向量类,替代pygame.math.Vector2
class Vector2:
    def __init__(self, x=0.0, y=0.0):
        self.x = float(x)
        self.y = float(y)
    
    def __add__(self, other):
        return Vector2(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector2(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector2(self.x * scalar, self.y * scalar)
    
    def __truediv__(self, scalar):
        return Vector2(self.x / scalar, self.y / scalar)
    
    def __repr__(self):
        return f"Vector2({self.x:.2f}, {self.y:.2f})"
    
    def distance_to(self, other):
        return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)

# 定义向量和重力
vector = Vector2
gravity = vector(0, 0.3)  # 模拟重力效果
DISPLAY_WIDTH = local.VIDEO_WIDTH
DISPLAY_HEIGHT = local.VIDEO_HEIGHT  # 窗口大小

# 定义尾迹颜色
trail_colours = [(45, 45, 45), (60, 60, 60), (75, 75, 75),
                 (125, 125, 125), (150, 150, 150)]
dynamic_offset = 1  # 动态尾迹偏移
static_offset = 5  # 静态尾迹偏移

class Firework:
    def __init__(self):
        # 初始化烟花的颜色和粒子
        self.colour = tuple(random.randint(0, 255) for _ in range(3))  # 随机颜色
        self.colours = [tuple(random.randint(0, 255) for _ in range(3)) for _ in range(3)]  # 粒子颜色
        self.firework = Particle(random.randint(0, DISPLAY_WIDTH), DISPLAY_HEIGHT, True, self.colour)  # 创建烟花粒子
        self.exploded = False  # 标记烟花是否已爆炸
        self.particles = []  # 存储爆炸后的粒子
        self.min_max_particles = vector(200, 300)  # 粒子数量范围

    def update(self, canvas):
        # 更新烟花状态
        if not self.exploded:
            self.firework.apply_force(gravity)  # 应用重力
            self.firework.move()  # 移动烟花粒子
            for tf in self.firework.trails:
                tf.show(canvas)  # 显示尾迹

            self.show(canvas)  # 显示烟花

            if self.firework.vel.y >= 0:  # 检查烟花是否达到最高点
                self.exploded = True  # 标记为已爆炸
                self.explode()  # 执行爆炸

        else:
            # 更新爆炸后的粒子
            for particle in self.particles:
                particle.apply_force(vector(gravity.x + random.uniform(-0.1, 0.1), 
                                           gravity.y / 2 + random.uniform(0.01, 0.08)))
                particle.move()  # 移动粒子
                for t in particle.trails:
                    t.show(canvas)  # 显示粒子的尾迹
                particle.show(canvas)  # 显示粒子

    def explode(self):
        # 生成爆炸后的粒子
        amount = random.randint(int(self.min_max_particles.x), int(self.min_max_particles.y))
        self.particles.extend(
            Particle(self.firework.pos.x, self.firework.pos.y, False, self.colours) 
            for _ in range(amount)
        )

    def show(self, canvas):
        # 在画布上绘制烟花
        cv2.circle(canvas, 
                  (int(self.firework.pos.x), int(self.firework.pos.y)), 
                  self.firework.size, 
                  self.colour, 
                  -1)

    def remove(self):
        # 移除已标记的粒子
        self.particles = [p for p in self.particles if not p.remove]
        return self.exploded and not self.particles  # 返回是否可以移除烟花

class Particle:
    def __init__(self, x, y, firework, colour):
        # 初始化粒子的属性
        self.firework = firework
        self.pos = vector(x, y)  # 当前粒子位置
        self.origin = vector(x, y)  # 粒子起始位置
        self.radius = 20  # 粒子半径
        self.remove = False  # 标记粒子是否需要移除
        self.explosion_radius = random.randint(10, 25)  # 随机爆炸半径
        self.life = 0  # 粒子生命周期
        self.acc = vector(0, 0)  # 粒子加速度
        # 创建尾迹
        self.trails = [Trail(i, 5 if firework else random.randint(2, 4), firework) for i in range(5)]
        self.prev_posx = [-10] * 10  # 存储前10帧的x坐标
        self.prev_posy = [-10] * 10  # 存储前10帧的y坐标

        # 根据粒子类型设置速度、大小和颜色
        if self.firework:
            self.vel = vector(0, -random.randint(12, 16))  # 烟花粒子向上发射
            self.size = 5  # 烟花粒子大小
            self.colour = colour  # 烟花颜色
        else:
            # 普通粒子随机速度和大小
            self.vel = vector(random.uniform(-2, 2), random.uniform(-2, 2))  # 随机初始速度
            self.vel *= random.randint(10, self.explosion_radius + 5)  # 根据爆炸半径调整速度
            self.size = random.randint(3, 5)  # 随机粒子大小
            self.colour = random.choice(colour)  # 从颜色列表中随机选择颜色

    def apply_force(self, force):
        # 应用外力到粒子的加速度
        self.acc += force

    def move(self):
        # 更新粒子的位置和状态
        if not self.firework:
            self.vel *= 0.9  # 普通粒子速度衰减

        self.vel += self.acc  # 更新速度
        self.pos += self.vel  # 更新位置
        self.acc *= 0  # 重置加速度

        # 检查普通粒子是否超出爆炸半径
        if self.life == 0 and not self.firework:
            distance = self.pos.distance_to(self.origin)
            if distance > self.explosion_radius:
                self.remove = True  # 超出范围则标记为移除

        self.decay()  # 处理粒子的衰减
        self.trail_update()  # 更新尾迹
        self.life += 1  # 增加生命周期

    def show(self, canvas):
        # 在画布上绘制粒子
        cv2.circle(canvas, 
                  (int(self.pos.x), int(self.pos.y)), 
                  self.size, 
                  self.colour, 
                  -1)

    def decay(self):
        # 根据粒子的生命周期决定是否移除
        if 70 > self.life > 20:  # 在20到70之间的粒子
            if random.randint(0, 30) == 0:  # 有小概率移除
                self.remove = True
        elif self.life > 70:  # 超过70的粒子
            if random.randint(0, 5) == 0:  # 有更高概率移除
                self.remove = True

    def trail_update(self):
        # 更新粒子的尾迹位置
        self.prev_posx.pop()  # 移除最旧的x坐标
        self.prev_posx.insert(0, int(self.pos.x))  # 插入当前x坐标
        self.prev_posy.pop()  # 移除最旧的y坐标
        self.prev_posy.insert(0, int(self.pos.y))  # 插入当前y坐标

        # 更新每个尾迹的位置
        for n, t in enumerate(self.trails):
            if t.dynamic:
                t.get_pos(self.prev_posx[n + dynamic_offset], self.prev_posy[n + dynamic_offset])
            else:
                t.get_pos(self.prev_posx[n + static_offset], self.prev_posy[n + static_offset])

class Trail:
    def __init__(self, n, size, dynamic):
        # 初始化尾迹的属性
        self.pos_in_line = n  # 尾迹在粒子尾迹中的位置索引
        self.pos = vector(-10, -10)  # 尾迹的初始位置,设置为无效值
        self.dynamic = dynamic  # 布尔值,指示尾迹是否为动态
        # 根据尾迹的动态性设置颜色
        self.colour = trail_colours[n] if dynamic else (255, 255, 200)  # 动态尾迹使用预定义颜色,静态尾迹为淡黄色
        # 根据尾迹的动态性和位置设置大小
        self.size = max(size - n // 2, 0) if dynamic else max(size - 2, 0)  # 动态尾迹大小随位置变化,静态尾迹大小固定

    def get_pos(self, x, y):
        # 更新尾迹的位置
        self.pos = vector(x, y)  # 将尾迹位置设置为传入的坐标

    def show(self, canvas):
        # 在画布上绘制尾迹
        cv2.circle(canvas, 
                  (int(self.pos.x), int(self.pos.y)), 
                  self.size, 
                  self.colour, 
                  -1)

def update_fireworks(canvas, fireworks):
    """更新所有烟花状态"""
    
    # 清空画布(使用深灰色背景)
    canvas[:] = (20, 20, 30)
    
    # 更新所有烟花
    fireworks_to_remove = []
    for fw in fireworks:
        fw.update(canvas)
        if fw.remove():
            fireworks_to_remove.append(fw)
    
    # 移除需要删除的烟花
    for fw in fireworks_to_remove:
        fireworks.remove(fw)
    
    return canvas

def main():
    """带窗口显示的主函数"""
    
    # 创建OpenCV窗口
    window_name = 'Fireworks'
    cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
    
    # 设置为全屏
    cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
    cv2.resizeWindow(window_name, DISPLAY_WIDTH, DISPLAY_HEIGHT)
    
    # 创建画布
    canvas = np.zeros((DISPLAY_HEIGHT, DISPLAY_WIDTH, 3), dtype=np.uint8)
    
    # 初始化烟花
    fireworks = [Firework() for _ in range(2)]
    
    # 使用time模块控制帧率
    frame_time = 1.0 / 30.0  # 30 FPS
    running = True
    frame_count = 0
    
    print("Fireworks 程序启动")
    print("按 ESC 或 Q 键退出")
    print("按 R 键重置烟花")
    print("按 S 键保存当前帧")
    
    last_time = time.time()
    
    while running:
        # 计算帧时间
        current_time = time.time()
        elapsed = current_time - last_time
        
        # 控制帧率
        if elapsed < frame_time:
            time.sleep(frame_time - elapsed)
            current_time = time.time()
        
        last_time = current_time
        frame_count += 1
        
        # 清空画布
        canvas[:] = (20, 20, 30)
        
        # 随机添加新烟花
        if random.randint(0, 20) == 1:
            fireworks.append(Firework())
        
        # 更新所有烟花
        fireworks_to_remove = []
        for fw in fireworks:
            fw.update(canvas)
            if fw.remove():
                fireworks_to_remove.append(fw)
        
        # 移除需要删除的烟花
        for fw in fireworks_to_remove:
            fireworks.remove(fw)
        
        # 显示图像
        cv2.imshow(window_name, canvas)
        
        # 处理按键
        key = cv2.waitKey(1) & 0xFF
        
        if key == ord('q') or key == 27:  # Q或ESC
            running = False
        elif key == ord('r'):  # 重置烟花
            fireworks = [Firework() for _ in range(2)]
            print(f"烟花已重置 ({frame_count}帧)")
        elif key == ord('s'):  # 保存当前帧
            filename = f"fireworks_{frame_count}.png"
            cv2.imwrite(filename, canvas)
            print(f"已保存: {filename}")
        elif key == ord('f'):  # 切换全屏
            current = cv2.getWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN)
            new_value = cv2.WINDOW_NORMAL if current == cv2.WINDOW_FULLSCREEN else cv2.WINDOW_FULLSCREEN
            cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, new_value)
        
        # 每100帧显示状态
        if frame_count % 100 == 0:
            print(f"帧: {frame_count}, 烟花数量: {len(fireworks)}")
    
    cv2.destroyAllWindows()
    print(f"程序退出,总共运行 {frame_count} 帧")


def debug_colors():
    """调试颜色显示"""
    # 创建测试图像
    test_img = np.zeros((200, 600, 3), dtype=np.uint8)
    
    # BGR颜色测试
    colors = [
        ("Blue", (255, 0, 0)),     # 蓝色 (B=255)
        ("Green", (0, 255, 0)),    # 绿色 (G=255)
        ("Red", (0, 0, 255)),      # 红色 (R=255)
        ("Yellow", (0, 255, 255)), # 黄色 (G=255, R=255)
        ("Cyan", (255, 255, 0)),   # 青色 (B=255, G=255)
        ("Magenta", (255, 0, 255)) # 洋红 (B=255, R=255)
    ]
    
    for i, (name, color) in enumerate(colors):
        x1, x2 = i*100, (i+1)*100
        cv2.rectangle(test_img, (x1, 50), (x2, 150), color, -1)
        cv2.putText(test_img, name, (x1+10, 180), 
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1)
    
    cv2.imshow('Color Test (BGR format)', test_img)
    print("颜色测试窗口已打开,按任意键关闭")
    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    # 先运行颜色测试
    # debug_colors()
    
    # 选择运行模式
    print("=== Fireworks 程序 ===")
    print("1. 带窗口显示")
    print("2. 颜色测试")
    
    try:
        choice = int(input("请选择模式 (1-2): "))
    except:
        choice = 1
    
    if choice == 1:
        main()
    elif choice == 2:
        debug_colors()
    else:
        print("无效选择,使用默认模式 (带窗口)")
        main()
相关推荐
love530love2 小时前
让 ComfyUI 官方 CLI 在 Windows CMD 里也能 Tab 补全 —— 实测与避坑记录
人工智能·windows·python·clink·comfy-cli·命令补全·clickcompletion
Rabi'2 小时前
Windows系统 Qt 整合 OpenCV4.12.0
开发语言·windows·qt·opencv
88号技师2 小时前
2025年10月一区SCI-中心碰撞优化算法Centered Collision Optimizer-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
CodeCraft Studio2 小时前
国产化PDF处理控件Spire.PDF教程:在Java快速解析PDF文本、表格、图像和元数据
java·python·pdf·pdf解析·spire.pdf·元数据解析·java pdf解析
棒棒的皮皮2 小时前
【OpenCV】Python图像处理之形态学梯度运算
图像处理·python·opencv·计算机视觉
znhy_232 小时前
day43打卡
python
zore_c2 小时前
【数据结构】堆——超详解!!!(包含堆的实现)
c语言·开发语言·数据结构·经验分享·笔记·算法·链表
leo_2322 小时前
从开发语言角度来谈谈SMP(中)--SMP(软件制作平台)语言基础知识之十三
开发语言·开发工具·smp(软件制作平台)·应用系统
leo_2322 小时前
从开发语言角度来谈谈SMP(下)--SMP(软件制作平台)语言基础知识之十三
linux·运维·开发语言·开发工具·smp(软件制作平台)·应用系统