small-projects/algorithm-visualisations/particles.py
OusmBlueNinja 483643a4c0 algorithms
2025-04-07 11:53:24 -05:00

175 lines
7.2 KiB
Python

import pygame
import random
import math
# Initialize pygame
pygame.init()
# Screen dimensions and panel sizes
WIDTH, HEIGHT = 800, 600
PANEL_HEIGHT = 150 # Bottom panel for sliders
SIM_HEIGHT = HEIGHT - PANEL_HEIGHT # Simulation area height
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Particle Simulation with Adjustable Variables")
clock = pygame.time.Clock()
# Define a simple Slider class
class Slider:
def __init__(self, x, y, w, h, min_val, max_val, init_val, label):
self.x = x
self.y = y
self.w = w
self.h = h
self.min_val = min_val
self.max_val = max_val
self.value = init_val
self.label = label
self.dragging = False
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
if self.get_knob_rect().collidepoint(event.pos):
self.dragging = True
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
self.dragging = False
elif event.type == pygame.MOUSEMOTION:
if self.dragging:
mx = event.pos[0]
# Clamp mx inside slider range
relative = (mx - self.x) / self.w
relative = max(0, min(1, relative))
self.value = self.min_val + relative * (self.max_val - self.min_val)
def get_knob_rect(self):
# Determine knob position along slider track
relative = (self.value - self.min_val) / (self.max_val - self.min_val)
knob_x = self.x + relative * self.w
knob_width = 10
knob_height = self.h
return pygame.Rect(knob_x - knob_width // 2, self.y, knob_width, knob_height)
def draw(self, surface, font):
# Draw slider track (a thin rectangle)
track_rect = pygame.Rect(self.x, self.y + self.h // 2 - 2, self.w, 4)
pygame.draw.rect(surface, (200, 200, 200), track_rect)
# Draw knob
knob_rect = self.get_knob_rect()
pygame.draw.rect(surface, (100, 100, 100), knob_rect)
# Draw label and current value (as plain text)
label_surface = font.render(f"{self.label}: {self.value:.2f}", True, (0, 0, 0))
surface.blit(label_surface, (self.x, self.y - 20))
# Particle class
class Particle:
def __init__(self, pos, velocity, lifetime, size, color=(255, 255, 255)):
self.pos = pygame.math.Vector2(pos)
self.vel = pygame.math.Vector2(velocity)
self.lifetime = lifetime
self.size = size
self.initial_lifetime = lifetime
self.color = color
def update(self, gravity):
self.vel.y += gravity
self.pos += self.vel
self.lifetime -= 1
def is_dead(self):
return self.lifetime <= 0
def draw(self, surface, glow_intensity):
# Draw glow effect if glow_intensity is greater than zero
if glow_intensity > 0:
# Create a temporary surface for glow with per-pixel alpha
glow_surface = pygame.Surface((self.size*6, self.size*6), pygame.SRCALPHA)
glow_center = self.size * 3
# Draw several concentric circles for the glow effect
for i in range(1, 6):
# Alpha decreases as the circle radius increases
alpha = max(0, int(255 * (glow_intensity / 10) * (1 - i/6)))
radius = int(self.size + i * 2)
pygame.draw.circle(glow_surface,
(self.color[0], self.color[1], self.color[2], alpha),
(glow_center, glow_center),
radius)
# Blit the glow surface with additive blending
surface.blit(glow_surface, (self.pos.x - self.size*3, self.pos.y - self.size*3), special_flags=pygame.BLEND_RGBA_ADD)
# Draw the particle itself
pygame.draw.circle(surface, self.color, (int(self.pos.x), int(self.pos.y)), int(self.size))
# Create sliders for each variable, arranged in two rows within the bottom panel.
font = pygame.font.SysFont("Arial", 16)
sliders = []
# Row 1: Emission Rate, Speed, Lifetime
sliders.append(Slider(x=20, y=SIM_HEIGHT + 10, w=200, h=20, min_val=0, max_val=50, init_val=5, label="Emission Rate"))
sliders.append(Slider(x=240, y=SIM_HEIGHT + 10, w=200, h=20, min_val=0, max_val=10, init_val=5, label="Speed"))
sliders.append(Slider(x=460, y=SIM_HEIGHT + 10, w=200, h=20, min_val=10, max_val=300, init_val=100, label="Lifetime"))
# Row 2: Size, Gravity, Glow
sliders.append(Slider(x=20, y=SIM_HEIGHT + 50, w=200, h=20, min_val=1, max_val=20, init_val=5, label="Size"))
sliders.append(Slider(x=240, y=SIM_HEIGHT + 50, w=200, h=20, min_val=0, max_val=2, init_val=0.1, label="Gravity"))
sliders.append(Slider(x=460, y=SIM_HEIGHT + 50, w=200, h=20, min_val=0, max_val=10, init_val=3, label="Glow"))
# List to hold all particles
particles = []
# Emitter position (center of simulation area)
emitter = pygame.math.Vector2(WIDTH // 2, SIM_HEIGHT // 2)
# Main loop
running = True
while running:
# Process events (for quitting and slider events)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for slider in sliders:
slider.handle_event(event)
# Get simulation variables from slider values
emission_rate = sliders[0].value # particles per frame
speed = sliders[1].value # initial speed magnitude
lifetime = sliders[2].value # lifetime in frames
size = sliders[3].value # particle size (radius)
gravity = sliders[4].value # gravity acceleration
glow = sliders[5].value # glow intensity
# Spawn new particles (round emission_rate to an integer count)
for _ in range(int(emission_rate)):
# Spawn at the emitter with a random direction
angle = random.uniform(0, 2 * math.pi)
velocity = pygame.math.Vector2(math.cos(angle), math.sin(angle)) * speed
particles.append(Particle(emitter, velocity, lifetime, size))
# Update particles and remove dead ones
for particle in particles[:]:
particle.update(gravity)
if particle.is_dead():
particles.remove(particle)
# Clear simulation area (fill with black)
screen.fill((0, 0, 0), rect=pygame.Rect(0, 0, WIDTH, SIM_HEIGHT))
# Optionally fill the control area with a light color (or leave it as is)
screen.fill((220, 220, 220), rect=pygame.Rect(0, SIM_HEIGHT, WIDTH, PANEL_HEIGHT))
# Draw all particles
for particle in particles:
particle.draw(screen, glow_intensity=glow)
# Draw slider controls in the bottom panel
for slider in sliders:
slider.draw(screen, font)
# Draw debug text (plain text, no background) at the top-left corner of the simulation area
debug_text = (f"Particles: {len(particles)} | Emission Rate: {emission_rate:.2f} | Speed: {speed:.2f} | "
f"Lifetime: {lifetime:.2f} | Size: {size:.2f} | Gravity: {gravity:.2f} | Glow: {glow:.2f}")
debug_surface = font.render(debug_text, True, (255, 255, 255))
screen.blit(debug_surface, (10, 10))
pygame.display.flip()
clock.tick(60)
pygame.quit()