From 553a89f2d4204b882591b5e74927ee1bc5ff8fcd Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 25 Jan 2026 18:35:10 +0100 Subject: [PATCH] Synchronize confetti.py --- .../META-INF/MANIFEST.JSON | 10 +- .../assets/confetti.py | 237 +++++++++++------- .../assets/confetti_app.py | 28 +++ .../res/drawable-mdpi/confetti0.png | Bin 0 -> 5361 bytes .../res/drawable-mdpi/confetti4.png | Bin 0 -> 2711 bytes 5 files changed, 186 insertions(+), 89 deletions(-) create mode 100644 internal_filesystem/apps/com.micropythonos.confetti/assets/confetti_app.py create mode 100644 internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti0.png create mode 100644 internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti4.png diff --git a/internal_filesystem/apps/com.micropythonos.confetti/META-INF/MANIFEST.JSON b/internal_filesystem/apps/com.micropythonos.confetti/META-INF/MANIFEST.JSON index 85dedb6d..858743d4 100644 --- a/internal_filesystem/apps/com.micropythonos.confetti/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/com.micropythonos.confetti/META-INF/MANIFEST.JSON @@ -3,15 +3,15 @@ "publisher": "MicroPythonOS", "short_description": "Just shows confetti", "long_description": "Nothing special, just a demo.", -"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/icons/com.micropythonos.confetti_0.0.3_64x64.png", -"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/mpks/com.micropythonos.confetti_0.0.3.mpk", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/icons/com.micropythonos.confetti_0.0.4_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/mpks/com.micropythonos.confetti_0.0.4.mpk", "fullname": "com.micropythonos.confetti", -"version": "0.0.3", +"version": "0.0.4", "category": "games", "activities": [ { - "entrypoint": "assets/confetti.py", - "classname": "Confetti", + "entrypoint": "assets/confetti_app.py", + "classname": "ConfettiApp", "intent_filters": [ { "action": "main", diff --git a/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py b/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py index fd90745d..f79e4d62 100644 --- a/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py +++ b/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py @@ -2,114 +2,183 @@ import time import random import lvgl as lv -from mpos import Activity, Intent, config -from mpos.ui import task_handler +from mpos import DisplayMetrics -class Confetti(Activity): - # === CONFIG === - SCREEN_WIDTH = 320 - SCREEN_HEIGHT = 240 - ASSET_PATH = "M:apps/com.micropythonos.confetti/res/drawable-mdpi/" - MAX_CONFETTI = 21 - GRAVITY = 100 # pixels/sec² - - def onCreate(self): - print("Confetti Activity starting...") - - # Background - self.screen = lv.obj() - self.screen.set_style_bg_color(lv.color_hex(0x000033), 0) # Dark blue - self.screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) - self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) - - # Timing +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 self.last_time = time.ticks_ms() - - # Confetti state self.confetti_pieces = [] self.confetti_images = [] - self.used_img_indices = set() # Track which image slots are in use + 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 + # Pre-create LVGL image objects - for i in range(self.MAX_CONFETTI): - img = lv.image(self.screen) - img.set_src(f"{self.ASSET_PATH}confetti{random.randint(1,3)}.png") + 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") img.add_flag(lv.obj.FLAG.HIDDEN) self.confetti_images.append(img) - - # Spawn initial confetti - for _ in range(self.MAX_CONFETTI): - self.spawn_confetti() - - self.setContentView(self.screen) - - def onResume(self, screen): - task_handler.add_event_cb(self.update_frame, task_handler.TASK_HANDLER_STARTED) - - def onPause(self, screen): - task_handler.remove_event_cb(self.update_frame) - - def spawn_confetti(self): - """Safely spawn a new confetti piece with unique img_idx""" - # 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(-10, self.SCREEN_WIDTH + 10), - 'y': random.uniform(50, 150), - 'vx': random.uniform(-100, 100), - 'vy': random.uniform(-150, -80), - 'spin': random.uniform(-400, 400), - 'age': 0.0, - 'lifetime': random.uniform(1.8, 5), - 'rotation': random.uniform(0, 360), - 'scale': 1.0 - } - self.confetti_pieces.append(piece) - self.used_img_indices.add(idx) - - def update_frame(self, a, b): + + 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.""" current_time = time.ticks_ms() - delta_ms = time.ticks_diff(current_time, self.last_time) - delta_time = delta_ms / 1000.0 + delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0 self.last_time = current_time - + + # === 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 === new_pieces = [] - for piece in self.confetti_pieces: - # === UPDATE PHYSICS === + # Physics 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) - - # === UPDATE LVGL IMAGE === + + # Render img = self.confetti_images[piece['img_idx']] img.remove_flag(lv.obj.FLAG.HIDDEN) img.set_pos(int(piece['x']), int(piece['y'])) - img.set_rotation(int(piece['rotation'] * 10)) # LVGL: 0.1 degrees - img.set_scale(int(256 * piece['scale']* 2)) # 256 = 100% - - # === CHECK IF DEAD === - off_screen = ( - piece['x'] < -60 or piece['x'] > self.SCREEN_WIDTH + 60 or - piece['y'] > self.SCREEN_HEIGHT + 60 + 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'] ) - too_old = piece['age'] > piece['lifetime'] - - if off_screen or too_old: + + if dead: img.add_flag(lv.obj.FLAG.HIDDEN) self.used_img_indices.discard(piece['img_idx']) - self.spawn_confetti() # Replace immediately else: new_pieces.append(piece) - - # === APPLY NEW LIST === + self.confetti_pieces = new_pieces + + # 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) diff --git a/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti_app.py b/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti_app.py new file mode 100644 index 00000000..23336e63 --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti_app.py @@ -0,0 +1,28 @@ +import time +import random +import lvgl as lv + +from mpos import Activity + +from confetti import Confetti + +class ConfettiApp(Activity): + + ASSET_PATH = "M:apps/com.micropythonos.confetti/res/drawable-mdpi/" + ICON_PATH = "M:apps/com.lightningpiggy.displaywallet/res/mipmap-mdpi/" + confetti_duration = 60 * 1000 + + confetti = None + + def onCreate(self): + main_screen = lv.obj() + self.confetti = Confetti(main_screen, self.ICON_PATH, self.ASSET_PATH, self.confetti_duration) + print("created ", self.confetti) + self.setContentView(main_screen) + + def onResume(self, screen): + print("onResume") + self.confetti.start() + + def onPause(self, screen): + self.confetti.stop() diff --git a/internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti0.png b/internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti0.png new file mode 100644 index 0000000000000000000000000000000000000000..220c65cbf762d1bf206c7000d2bb48a1ded52134 GIT binary patch literal 5361 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 literal 0 HcmV?d00001 diff --git a/internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti4.png b/internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti4.png new file mode 100644 index 0000000000000000000000000000000000000000..bccb6d99434e4bcd5583b5cd72ac2f73db5fc0f9 GIT binary patch literal 2711 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