Files

187 lines
6.5 KiB
Python
Raw Permalink Normal View History

# This is a copy of LightningPiggyApp's confetti.py
2025-11-12 11:56:55 +01:00
import time
import random
import lvgl as lv
2026-01-25 18:35:10 +01:00
from mpos import DisplayMetrics
2025-11-12 11:56:55 +01:00
2026-01-25 18:35:10 +01:00
class Confetti:
"""Manages confetti animation with physics simulation."""
def __init__(self, screen, icon_path, asset_path, duration=10000):
"""
Initialize the Confetti system.
Args:
screen: The LVGL screen/display object
icon_path: Path to icon assets (e.g., "M:apps/com.lightningpiggy.displaywallet/res/mipmap-mdpi/")
asset_path: Path to confetti assets (e.g., "M:apps/com.lightningpiggy.displaywallet/res/drawable-mdpi/")
max_confetti: Maximum number of confetti pieces to display
"""
self.screen = screen
self.icon_path = icon_path
self.asset_path = asset_path
self.duration = duration
self.max_confetti = 21
# Physics constants
self.GRAVITY = 100 # pixels/sec²
# Screen dimensions
self.screen_width = DisplayMetrics.width()
self.screen_height = DisplayMetrics.height()
# State
self.is_running = False
2025-11-12 11:56:55 +01:00
self.last_time = time.ticks_ms()
self.confetti_pieces = []
self.confetti_images = []
2026-01-25 18:35:10 +01:00
self.used_img_indices = set()
self.update_timer = None # Reference to LVGL timer for frame updates
# Spawn control
self.spawn_timer = 0
self.spawn_interval = 0.15 # seconds
self.animation_start = 0
2025-11-12 11:56:55 +01:00
2026-01-25 18:35:10 +01:00
2025-11-12 11:56:55 +01:00
# Pre-create LVGL image objects
2026-01-25 18:35:10 +01:00
self._init_images()
def _init_images(self):
"""Pre-create LVGL image objects for confetti."""
iconimages = 2
for _ in range(iconimages):
img = lv.image(lv.layer_top())
img.set_src(f"{self.icon_path}icon_64x64.png")
2025-11-12 11:56:55 +01:00
img.add_flag(lv.obj.FLAG.HIDDEN)
self.confetti_images.append(img)
2026-01-25 18:35:10 +01:00
for i in range(self.max_confetti - iconimages):
img = lv.image(lv.layer_top())
img.set_src(f"{self.asset_path}confetti{random.randint(0, 4)}.png")
img.add_flag(lv.obj.FLAG.HIDDEN)
self.confetti_images.append(img)
def start(self):
"""Start the confetti animation."""
if self.is_running:
return
self.is_running = True
self.last_time = time.ticks_ms()
self._clear_confetti()
# Staggered spawn control
self.spawn_timer = 0
self.animation_start = time.ticks_ms() / 1000.0
# Initial burst
for _ in range(10):
self._spawn_one()
self.update_timer = lv.timer_create(self._update_frame, 16, None) # max 60 fps = 16ms/frame
# Stop spawning after duration
lv.timer_create(self.stop, self.duration, None).set_repeat_count(1)
def stop(self, timer=None):
"""Stop the confetti animation."""
self.is_running = False
def _clear_confetti(self):
"""Clear all confetti pieces from the screen."""
for img in self.confetti_images:
img.add_flag(lv.obj.FLAG.HIDDEN)
self.confetti_pieces = []
self.used_img_indices.clear()
def _update_frame(self, timer):
"""Update frame for confetti animation. Called by LVGL timer."""
2025-11-12 11:56:55 +01:00
current_time = time.ticks_ms()
2026-01-25 18:35:10 +01:00
delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0
2025-11-12 11:56:55 +01:00
self.last_time = current_time
2026-01-25 18:35:10 +01:00
# === STAGGERED SPAWNING ===
if self.is_running:
self.spawn_timer += delta_time
if self.spawn_timer >= self.spawn_interval:
self.spawn_timer = 0
for _ in range(random.randint(1, 2)):
if len(self.confetti_pieces) < self.max_confetti:
self._spawn_one()
# === UPDATE ALL PIECES ===
2025-11-12 11:56:55 +01:00
new_pieces = []
for piece in self.confetti_pieces:
2026-01-25 18:35:10 +01:00
# Physics
2025-11-12 11:56:55 +01:00
piece['age'] += delta_time
piece['x'] += piece['vx'] * delta_time
piece['y'] += piece['vy'] * delta_time
piece['vy'] += self.GRAVITY * delta_time
piece['rotation'] += piece['spin'] * delta_time
piece['scale'] = max(0.3, 1.0 - (piece['age'] / piece['lifetime']) * 0.7)
2026-01-25 18:35:10 +01:00
# Render
2025-11-12 11:56:55 +01:00
img = self.confetti_images[piece['img_idx']]
img.remove_flag(lv.obj.FLAG.HIDDEN)
img.set_pos(int(piece['x']), int(piece['y']))
2026-01-25 18:35:10 +01:00
img.set_rotation(int(piece['rotation'] * 10))
orig = img.get_width()
if orig >= 64:
img.set_scale(int(256 * piece['scale'] / 1.5))
elif orig < 32:
img.set_scale(int(256 * piece['scale'] * 1.5))
else:
img.set_scale(int(256 * piece['scale']))
# Death check
dead = (
piece['x'] < -60 or piece['x'] > self.screen_width + 60 or
piece['y'] > self.screen_height + 60 or
piece['age'] > piece['lifetime']
2025-11-12 11:56:55 +01:00
)
2026-01-25 18:35:10 +01:00
if dead:
2025-11-12 11:56:55 +01:00
img.add_flag(lv.obj.FLAG.HIDDEN)
self.used_img_indices.discard(piece['img_idx'])
else:
new_pieces.append(piece)
2026-01-25 18:35:10 +01:00
2025-11-12 11:56:55 +01:00
self.confetti_pieces = new_pieces
2026-01-25 18:35:10 +01:00
# Full stop when empty and paused
if not self.confetti_pieces and not self.is_running:
print("Confetti finished")
if self.update_timer:
self.update_timer.delete()
self.update_timer = None
def _spawn_one(self):
"""Spawn a single confetti piece."""
if not self.is_running:
return
# Find a free image slot
for idx, img in enumerate(self.confetti_images):
if img.has_flag(lv.obj.FLAG.HIDDEN) and idx not in self.used_img_indices:
break
else:
return # No free slot
piece = {
'img_idx': idx,
'x': random.uniform(-50, self.screen_width + 50),
'y': random.uniform(50, 100), # Start above screen
'vx': random.uniform(-80, 80),
'vy': random.uniform(-150, 0),
'spin': random.uniform(-500, 500),
'age': 0.0,
'lifetime': random.uniform(5.0, 10.0), # Long enough to fill 10s
'rotation': random.uniform(0, 360),
'scale': 1.0
}
self.confetti_pieces.append(piece)
self.used_img_indices.add(idx)