You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Synchronize confetti.py
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user