Skip to content

Instantly share code, notes, and snippets.

@maduck
Last active September 12, 2024 14:11
Show Gist options
  • Save maduck/3ef9a27d6571c4f66726067ddb8bacad to your computer and use it in GitHub Desktop.
Save maduck/3ef9a27d6571c4f66726067ddb8bacad to your computer and use it in GitHub Desktop.
a very small learning project to have a snowing screensaver.
#!/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