107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""Fish tank screensaver.
|
|
|
|
Burn-in safe for the CRT: the background is pure black and every drawn
|
|
element is always in motion. Fish images are loaded from the fish folder
|
|
and are assumed to face right; a fish swimming left gets flipped
|
|
horizontally. Bubbles rise from each fish's mouth.
|
|
"""
|
|
|
|
import math
|
|
import random
|
|
|
|
import pygame
|
|
|
|
FISH_MIN_H, FISH_MAX_H = 60, 140
|
|
FISH_MIN_SPEED, FISH_MAX_SPEED = 40, 130 # px/sec
|
|
BUBBLE_COLOR = (150, 200, 255)
|
|
|
|
|
|
class Bubble:
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
self.radius = random.uniform(2, 6)
|
|
self.speed = random.uniform(30, 70)
|
|
self.wobble_phase = random.uniform(0, math.tau)
|
|
self.wobble_amp = random.uniform(2, 8)
|
|
|
|
def update(self, dt):
|
|
self.y -= self.speed * dt
|
|
self.wobble_phase += dt * 3
|
|
|
|
def draw(self, screen):
|
|
x = self.x + math.sin(self.wobble_phase) * self.wobble_amp
|
|
pygame.draw.circle(screen, BUBBLE_COLOR, (int(x), int(self.y)),
|
|
int(self.radius), width=1)
|
|
|
|
|
|
class Fish:
|
|
def __init__(self, image, screen_size):
|
|
self.screen_w, self.screen_h = screen_size
|
|
height = random.randint(FISH_MIN_H, FISH_MAX_H)
|
|
width = int(image.get_width() * height / image.get_height())
|
|
self.image_right = pygame.transform.smoothscale(image, (width, height))
|
|
self.image_left = pygame.transform.flip(self.image_right, True, False)
|
|
self.w, self.h = width, height
|
|
self.speed = random.uniform(FISH_MIN_SPEED, FISH_MAX_SPEED)
|
|
self.direction = random.choice([-1, 1])
|
|
self.x = random.uniform(0, self.screen_w - width)
|
|
self.base_y = random.uniform(0, self.screen_h - height)
|
|
self.bob_phase = random.uniform(0, math.tau)
|
|
self.bob_amp = random.uniform(5, 20)
|
|
self.bubble_timer = random.uniform(1, 4)
|
|
|
|
@property
|
|
def y(self):
|
|
return self.base_y + math.sin(self.bob_phase) * self.bob_amp
|
|
|
|
def mouth_pos(self):
|
|
mouth_x = self.x + self.w if self.direction > 0 else self.x
|
|
return mouth_x, self.y + self.h * 0.45
|
|
|
|
def update(self, dt, bubbles):
|
|
self.x += self.speed * self.direction * dt
|
|
self.bob_phase += dt * random.uniform(0.8, 1.2)
|
|
|
|
# Wrap around: swim fully off one edge, re-enter from the other
|
|
# at a fresh depth so fish never sit still or trace fixed lines.
|
|
if self.direction > 0 and self.x > self.screen_w:
|
|
self.x = -self.w
|
|
self.base_y = random.uniform(0, self.screen_h - self.h)
|
|
elif self.direction < 0 and self.x < -self.w:
|
|
self.x = self.screen_w
|
|
self.base_y = random.uniform(0, self.screen_h - self.h)
|
|
|
|
self.bubble_timer -= dt
|
|
if self.bubble_timer <= 0:
|
|
self.bubble_timer = random.uniform(1.5, 5)
|
|
mx, my = self.mouth_pos()
|
|
for _ in range(random.randint(1, 3)):
|
|
bubbles.append(Bubble(mx + random.uniform(-4, 4),
|
|
my + random.uniform(-4, 4)))
|
|
|
|
def draw(self, screen):
|
|
image = self.image_right if self.direction > 0 else self.image_left
|
|
screen.blit(image, (int(self.x), int(self.y)))
|
|
|
|
|
|
class FishTank:
|
|
def __init__(self, screen_size, fish_images, num_fish):
|
|
self.fish = [Fish(random.choice(fish_images), screen_size)
|
|
for _ in range(num_fish)]
|
|
self.bubbles = []
|
|
|
|
def update(self, dt):
|
|
for fish in self.fish:
|
|
fish.update(dt, self.bubbles)
|
|
for bubble in self.bubbles:
|
|
bubble.update(dt)
|
|
self.bubbles = [b for b in self.bubbles if b.y > -10]
|
|
|
|
def draw(self, screen):
|
|
screen.fill((0, 0, 0))
|
|
for bubble in self.bubbles:
|
|
bubble.draw(screen)
|
|
for fish in self.fish:
|
|
fish.draw(screen)
|