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()