Last active
September 12, 2024 14:11
-
-
Save maduck/3ef9a27d6571c4f66726067ddb8bacad to your computer and use it in GitHub Desktop.
a very small learning project to have a snowing screensaver.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
"""a very small learning project to have a snowing screensaver.""" | |
import random | |
import pygame.gfxdraw | |
class Snowflake: | |
"""Representation of a single snowflake.""" | |
MEAN_SIZE = 3 | |
MAX_SIZE = 15 | |
def __init__(self, max_x: int, max_y: int, speed_multiplier: float = 1) -> None: | |
""" | |
create a new snowflake | |
:param max_x: screen width to draw on | |
:param max_y: screen HEIGHT to draw on | |
""" | |
self.x = None | |
self.y = None | |
self.r = None | |
self.color = None | |
self.max_x = max_x | |
self.max_y = max_y | |
self.speed_multiplier = speed_multiplier | |
self.generate_new_parameters() | |
def generate_new_parameters(self) -> None: | |
""" | |
reposition a snowflake above the actual screen, | |
and resize it randomly | |
:return: None | |
""" | |
self.x = random.randrange(self.max_x) | |
self.y = random.randrange(-self.max_y, 0) | |
self.r = random.randrange(1, self.MAX_SIZE) | |
self.r = int(min(random.expovariate(1 / self.MEAN_SIZE) + 1, self.MAX_SIZE)) | |
intensity = random.randint(50, 255) | |
self.color = pygame.Color(intensity, intensity, intensity, 0) | |
def progress_color(self) -> None: | |
""" | |
increase opacity as the flake falls down | |
:return: None | |
""" | |
alpha = int(255 * self.y / self.max_y) | |
alpha = sorted([50, alpha, 255])[1] | |
self.color.a = alpha | |
def move(self, wind: float, max_x: int, max_y: int) -> None: | |
""" | |
move a snowflake downwards, respecting horizontal wind | |
:param wind: wind speed for horizontal movement | |
:param max_x: screen width to draw on | |
:param max_y: screen HEIGHT to draw on | |
:return: None | |
""" | |
self.max_x = max_x | |
self.max_y = max_y | |
self.x += int(wind / self.r) * self.speed_multiplier | |
self.y += (self.r + 1) * self.speed_multiplier | |
self.enforce_periodic_boundaries() | |
self.progress_color() | |
def enforce_periodic_boundaries(self) -> None: | |
""" | |
let a snowflake appear on one side of the screen | |
if it disappears on the other. | |
Also let it "respawn" if it fell past the screen | |
:return: None | |
""" | |
if (self.x < 0 - self.r) or (self.x > (self.max_x + self.r)): | |
self.x = self.max_x - self.x | |
if self.y > self.max_y + self.r: | |
self.generate_new_parameters() | |
def __getitem__(self, key: int): | |
""" | |
Makes the class iterable. | |
Enables the use of *snowflake for the current parameters | |
:param key: index to get: 0=>x, 1=>y, 2=>r, 3=>color | |
:return: the value connected to the index | |
""" | |
current_state = (int(self.x), int(self.y), self.r, self.color) | |
return current_state[key] | |
class SnowingApp: | |
""" | |
provides the pygame abstraction to have a little snowstorm | |
""" | |
BACKGROUND_COLOR = pygame.color.THECOLORS['black'] | |
FPS: int = 120 | |
def __init__(self, canvas, snow_amount, wind_speed): | |
self.canvas = canvas | |
self.wind_speed = wind_speed | |
screen_size = pygame.display.Info() | |
self.screen_width = screen_size.current_w | |
self.screen_height = screen_size.current_h | |
self.clock = pygame.time.Clock() | |
self.snowflakes = [Snowflake(self.screen_width, self.screen_height, 30 / self.FPS) for _ in range(snow_amount)] | |
self.pause = False | |
def run(self): | |
""" | |
runs the game loop, and starts the snow | |
:return: None | |
""" | |
snowing = True | |
while snowing: | |
snowing = self.handle_events() | |
self.canvas.fill(self.BACKGROUND_COLOR) | |
self.wind_speed += random.random() - 0.5 | |
self.draw_and_move_snowflakes() | |
self.refresh_screen() | |
def draw_and_move_snowflakes(self): | |
""" | |
iterates over all flakes, and renders them to the screen | |
:return: None | |
""" | |
for flake in self.snowflakes: | |
pygame.gfxdraw.filled_circle(self.canvas, *flake) | |
if not self.pause: | |
flake.move(self.wind_speed, self.screen_width, self.screen_height) | |
def refresh_screen(self): | |
""" | |
redraw the screen, respects the configured FPS | |
:return: None | |
""" | |
pygame.display.update() | |
self.clock.tick(self.FPS) | |
def handle_events(self): | |
""" | |
handle keypress and external signals | |
coming from pygame | |
:return: Boolean status telling whether the app is still running or not | |
""" | |
snowing = True | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
snowing = False | |
elif event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_ESCAPE: | |
snowing = False | |
elif event.key == pygame.K_SPACE: | |
self.pause = not self.pause | |
elif event.type == pygame.VIDEORESIZE: | |
self.screen_width = event.w | |
self.screen_height = event.h | |
return snowing | |
def main(): | |
""" | |
main invoker for the above classes | |
:return: None | |
""" | |
pygame.init() | |
screen_mode = pygame.RESIZABLE | pygame.DOUBLEBUF | pygame.SRCALPHA | pygame.HWSURFACE | |
screen = pygame.display.set_mode((200, 200), screen_mode, 32) | |
app = SnowingApp(screen, snow_amount=2000, wind_speed=0) | |
app.run() | |
pygame.quit() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment