Initial commit: CRT photobooth
This commit is contained in:
112
falling_photos.py
Normal file
112
falling_photos.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""Falling photos backdrop for the idle/title screen.
|
||||
|
||||
Saved photos from the photos/ folder drift down from the top of the
|
||||
screen with a small white border, looping forever. They are drawn behind
|
||||
the title text. Thumbnails are loaded once and cached by file path; the
|
||||
cache is pruned when photos are deleted (after a print cycle).
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
import pygame
|
||||
|
||||
THUMB_H = 140
|
||||
BORDER = 6
|
||||
NUM_PHOTOS = 10
|
||||
MIN_SPEED, MAX_SPEED = 30, 90 # px/sec downward
|
||||
DRIFT_AMP = 40
|
||||
ROT_MIN, ROT_MAX = -12.5, 12.5 # degrees (halved)
|
||||
SPIN_MIN, SPIN_MAX = -10, 10 # deg/sec (halved)
|
||||
|
||||
|
||||
def _color_with_border(src):
|
||||
"""Scale src to THUMB_H tall, add a white BORDER, return (surf, w, h)."""
|
||||
w0, h0 = src.get_size()
|
||||
new_w = max(1, int(w0 * THUMB_H / h0))
|
||||
scaled = pygame.transform.smoothscale(src, (new_w, THUMB_H))
|
||||
bw, bh = new_w + 2 * BORDER, THUMB_H + 2 * BORDER
|
||||
surf = pygame.Surface((bw, bh), pygame.SRCALPHA)
|
||||
surf.fill((255, 255, 255, 255))
|
||||
surf.blit(scaled, (BORDER, BORDER))
|
||||
return surf, bw, bh
|
||||
|
||||
|
||||
class _Sprite:
|
||||
def __init__(self, screen_size, picker):
|
||||
self.screen_w, self.screen_h = screen_size
|
||||
self.picker = picker
|
||||
self._respawn(top=True)
|
||||
|
||||
def _respawn(self, top=False):
|
||||
self.image, self.w, self.h = self.picker()
|
||||
self.angle = random.uniform(ROT_MIN, ROT_MAX)
|
||||
self.spin = random.uniform(SPIN_MIN, SPIN_MAX)
|
||||
self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
|
||||
self.drift_amp = random.uniform(0, DRIFT_AMP)
|
||||
self.drift_phase = random.uniform(0, math.tau)
|
||||
self.drift_speed = random.uniform(0.4, 1.2)
|
||||
if top:
|
||||
self.x = random.uniform(-self.w, self.screen_w)
|
||||
self.y = random.uniform(-self.h * 3, -self.h)
|
||||
else:
|
||||
self.x = random.uniform(0, self.screen_w - self.w)
|
||||
self.y = -self.h
|
||||
|
||||
def update(self, dt):
|
||||
self.y += self.speed * dt
|
||||
self.drift_phase += self.drift_speed * dt
|
||||
self.angle += self.spin * dt
|
||||
if self.y > self.screen_h + self.h:
|
||||
self._respawn()
|
||||
|
||||
def draw(self, screen):
|
||||
ox = math.sin(self.drift_phase) * self.drift_amp
|
||||
rotated = pygame.transform.rotate(self.image, self.angle)
|
||||
rect = rotated.get_rect(
|
||||
center=(self.x + self.w / 2 + ox, self.y + self.h / 2))
|
||||
screen.blit(rotated, rect)
|
||||
|
||||
|
||||
class FallingPhotos:
|
||||
def __init__(self, screen_size, photos_dir):
|
||||
self.screen_size = screen_size
|
||||
self.photos_dir = photos_dir
|
||||
self._cache = {} # path -> (bordered_surf, bw, bh)
|
||||
self._paths = []
|
||||
self._scan()
|
||||
self.sprites = [_Sprite(screen_size, self._pick) for _ in range(NUM_PHOTOS)]
|
||||
|
||||
def _scan(self):
|
||||
self._paths = (sorted(self.photos_dir.glob("*.jpg"))
|
||||
if self.photos_dir.exists() else [])
|
||||
self._cache = {p: v for p, v in self._cache.items() if p in self._paths}
|
||||
|
||||
def reload(self):
|
||||
"""Re-scan the photos folder (call after photos are deleted)."""
|
||||
self._scan()
|
||||
|
||||
def _pick(self):
|
||||
if not self._paths:
|
||||
surf = pygame.Surface((THUMB_H, THUMB_H), pygame.SRCALPHA)
|
||||
surf.fill((0, 0, 0, 0))
|
||||
return surf, THUMB_H, THUMB_H
|
||||
path = random.choice(self._paths)
|
||||
if path not in self._cache:
|
||||
try:
|
||||
src = pygame.image.load(str(path)).convert_alpha()
|
||||
except (pygame.error, OSError):
|
||||
self._paths = [p for p in self._paths if p != path]
|
||||
return self._pick()
|
||||
self._cache[path] = _color_with_border(src)
|
||||
return self._cache[path]
|
||||
|
||||
def update(self, dt):
|
||||
if not self._paths:
|
||||
self._scan()
|
||||
for s in self.sprites:
|
||||
s.update(dt)
|
||||
|
||||
def draw(self, screen):
|
||||
for s in self.sprites:
|
||||
s.draw(screen)
|
||||
Reference in New Issue
Block a user