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 00000000..220c65cb Binary files /dev/null and b/internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti0.png differ 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 00000000..bccb6d99 Binary files /dev/null and b/internal_filesystem/apps/com.micropythonos.confetti/res/drawable-mdpi/confetti4.png differ