187 lines
5.5 KiB
Python
187 lines
5.5 KiB
Python
# Using SDL2 library: # pip install PySDL2
|
|
|
|
import sys
|
|
import random
|
|
import time
|
|
import math
|
|
|
|
import sdl2
|
|
import sdl2.ext
|
|
|
|
FPS = 60
|
|
NEW_PARTICLES_PER_FRAME = 10
|
|
MAX_PARTICLES = 5_000
|
|
GRAVITY = 100
|
|
WIDTH = 640
|
|
HEIGHT = 480
|
|
|
|
|
|
def clamp(value, min_, max_):
|
|
"""Return value clamped between min and max"""
|
|
return max(min_, min(value, max_))
|
|
|
|
|
|
class Particle:
|
|
"""Particle obeying gravity law."""
|
|
|
|
def __init__(self):
|
|
self.x = 0
|
|
self.y = 0
|
|
self.v_x = 0
|
|
self.v_y = 0
|
|
|
|
def update(self, dtime: float) -> None:
|
|
"""Move particle and update speed with gravity"""
|
|
self.x = self.x + self.v_x * dtime
|
|
self.y = self.y + self.v_y * dtime
|
|
self.v_y = self.v_y + GRAVITY * dtime
|
|
|
|
def set(self, x, y, v_x, v_y):
|
|
"""Set particle values"""
|
|
self.x = x
|
|
self.y = y
|
|
self.v_x = v_x
|
|
self.v_y = v_y
|
|
|
|
|
|
class Fountain:
|
|
"""The fountain"""
|
|
|
|
def __init__(self, max_particles: int, particles_per_frame: int):
|
|
self.particles_per_frame = particles_per_frame
|
|
self.max_particles = max_particles
|
|
self.spread = 10.0
|
|
self.range = math.sqrt(2 * GRAVITY * (HEIGHT - 20 - self.spread))
|
|
self.saturation = 155
|
|
self.reciprocate = False
|
|
self.reciprocating_time = 0.0
|
|
self.particles = [
|
|
self.init_particle(Particle()) for _ in range(self.particles_per_frame)
|
|
]
|
|
|
|
def update(self, dtime) -> None:
|
|
"""Update particles"""
|
|
if self.reciprocate:
|
|
self.reciprocating_time += dtime
|
|
|
|
for particle in self.particles:
|
|
particle.update(dtime)
|
|
if particle.y > HEIGHT - 10:
|
|
self.init_particle(particle)
|
|
|
|
if len(self.particles) < self.max_particles:
|
|
for _ in range(self.particles_per_frame):
|
|
self.particles.append(self.init_particle(Particle()))
|
|
# print(len(particles))
|
|
|
|
def render(self, renderer: sdl2.ext.renderer.Renderer) -> None:
|
|
"""Render particles"""
|
|
points = [(particle.x, particle.y) for particle in self.particles]
|
|
|
|
renderer.clear()
|
|
renderer.draw_point(
|
|
points, sdl2.ext.Color(self.saturation, self.saturation, 255)
|
|
)
|
|
renderer.present()
|
|
|
|
def step_parameter(self, param, step):
|
|
"""Change parameters"""
|
|
if param == "spread":
|
|
self.spread = clamp(self.spread + step, 0, 50)
|
|
elif param == "range":
|
|
self.range = clamp(self.range + step, 0, 300)
|
|
elif param == "color":
|
|
self.saturation = clamp(self.saturation + step, 0, 255)
|
|
elif param == "reciprocate":
|
|
self.reciprocate = not self.reciprocate
|
|
self.reciprocating_time = 0.0
|
|
|
|
def init_particle(self, particle: Particle) -> Particle:
|
|
"""Move particle at initial position with a random-y speed"""
|
|
radius = random.random() * self.spread
|
|
direction = random.random() * math.pi * 2
|
|
v_x = radius * math.cos(direction) + math.sin(self.reciprocating_time) * 20.0
|
|
v_y = -self.range + radius * math.sin(direction)
|
|
particle.set(WIDTH // 2, HEIGHT - 10, v_x, v_y)
|
|
return particle
|
|
|
|
|
|
def make_renderer() -> sdl2.ext.renderer.Renderer:
|
|
"""Initialise SDL and make renderer"""
|
|
sdl2.ext.init()
|
|
|
|
window = sdl2.ext.Window("Particle Fountain", size=(WIDTH, HEIGHT))
|
|
window.show()
|
|
|
|
renderer = sdl2.ext.renderer.Renderer(window)
|
|
|
|
return renderer
|
|
|
|
|
|
def limit_frame_rate(fps: float, cur_time: int) -> bool:
|
|
"""Limit frame rate"""
|
|
dtime = time.monotonic_ns() - cur_time
|
|
frame_duration = 1e9 / fps
|
|
if dtime < frame_duration:
|
|
time.sleep((frame_duration - dtime) / 1e9)
|
|
return True
|
|
return False
|
|
|
|
|
|
def handle_events(fountain: Fountain):
|
|
"""Act on events"""
|
|
key_actions = {
|
|
sdl2.SDL_SCANCODE_PAGEUP: lambda: fountain.step_parameter("color", 5),
|
|
sdl2.SDL_SCANCODE_PAGEDOWN: lambda: fountain.step_parameter("color", -5),
|
|
sdl2.SDL_SCANCODE_UP: lambda: fountain.step_parameter("range", 1),
|
|
sdl2.SDL_SCANCODE_DOWN: lambda: fountain.step_parameter("range", -1),
|
|
sdl2.SDL_SCANCODE_LEFT: lambda: fountain.step_parameter("spread", -1),
|
|
sdl2.SDL_SCANCODE_RIGHT: lambda: fountain.step_parameter("spread", 1),
|
|
sdl2.SDL_SCANCODE_SPACE: lambda: fountain.step_parameter("reciprocate", 1),
|
|
}
|
|
|
|
events = sdl2.ext.get_events()
|
|
for event in events:
|
|
if event.type == sdl2.SDL_QUIT:
|
|
return False
|
|
if event.type == sdl2.SDL_KEYDOWN:
|
|
if event.key.keysym.scancode in key_actions:
|
|
key_actions[event.key.keysym.scancode]()
|
|
elif event.key.keysym.scancode == sdl2.SDL_SCANCODE_Q:
|
|
return False
|
|
return True
|
|
|
|
|
|
def main_loop(renderer: sdl2.ext.renderer.Renderer, fountain: Fountain) -> None:
|
|
"""Main animation loop"""
|
|
running = True
|
|
|
|
cur_time = time.monotonic_ns()
|
|
while running:
|
|
running = handle_events(fountain)
|
|
|
|
fountain.render(renderer)
|
|
|
|
if not limit_frame_rate(FPS, cur_time):
|
|
print(f"Didn't make it in time with {len(fountain.particles)} particles.")
|
|
|
|
dtime = (time.monotonic_ns() - cur_time) / 1e9 # in seconds
|
|
fountain.update(dtime)
|
|
cur_time = time.monotonic_ns()
|
|
|
|
sdl2.ext.quit()
|
|
|
|
|
|
def run():
|
|
"""Start!"""
|
|
|
|
renderer = make_renderer()
|
|
fountain = Fountain(MAX_PARTICLES, NEW_PARTICLES_PER_FRAME)
|
|
|
|
main_loop(renderer, fountain)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(run())
|