Skip to content

Commit 553a89f

Browse files
Synchronize confetti.py
1 parent 310c60a commit 553a89f

File tree

5 files changed

+188
-91
lines changed

5 files changed

+188
-91
lines changed

internal_filesystem/apps/com.micropythonos.confetti/META-INF/MANIFEST.JSON

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
"publisher": "MicroPythonOS",
44
"short_description": "Just shows confetti",
55
"long_description": "Nothing special, just a demo.",
6-
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/icons/com.micropythonos.confetti_0.0.3_64x64.png",
7-
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/mpks/com.micropythonos.confetti_0.0.3.mpk",
6+
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/icons/com.micropythonos.confetti_0.0.4_64x64.png",
7+
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.confetti/mpks/com.micropythonos.confetti_0.0.4.mpk",
88
"fullname": "com.micropythonos.confetti",
9-
"version": "0.0.3",
9+
"version": "0.0.4",
1010
"category": "games",
1111
"activities": [
1212
{
13-
"entrypoint": "assets/confetti.py",
14-
"classname": "Confetti",
13+
"entrypoint": "assets/confetti_app.py",
14+
"classname": "ConfettiApp",
1515
"intent_filters": [
1616
{
1717
"action": "main",

internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py

Lines changed: 155 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,114 +2,183 @@
22
import random
33
import lvgl as lv
44

5-
from mpos import Activity, Intent, config
6-
from mpos.ui import task_handler
7-
8-
class Confetti(Activity):
9-
# === CONFIG ===
10-
SCREEN_WIDTH = 320
11-
SCREEN_HEIGHT = 240
12-
ASSET_PATH = "M:apps/com.micropythonos.confetti/res/drawable-mdpi/"
13-
MAX_CONFETTI = 21
14-
GRAVITY = 100 # pixels/sec²
15-
16-
def onCreate(self):
17-
print("Confetti Activity starting...")
18-
19-
# Background
20-
self.screen = lv.obj()
21-
self.screen.set_style_bg_color(lv.color_hex(0x000033), 0) # Dark blue
22-
self.screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
23-
self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE)
24-
25-
# Timing
5+
from mpos import DisplayMetrics
6+
7+
class Confetti:
8+
"""Manages confetti animation with physics simulation."""
9+
10+
def __init__(self, screen, icon_path, asset_path, duration=10000):
11+
"""
12+
Initialize the Confetti system.
13+
14+
Args:
15+
screen: The LVGL screen/display object
16+
icon_path: Path to icon assets (e.g., "M:apps/com.lightningpiggy.displaywallet/res/mipmap-mdpi/")
17+
asset_path: Path to confetti assets (e.g., "M:apps/com.lightningpiggy.displaywallet/res/drawable-mdpi/")
18+
max_confetti: Maximum number of confetti pieces to display
19+
"""
20+
self.screen = screen
21+
self.icon_path = icon_path
22+
self.asset_path = asset_path
23+
self.duration = duration
24+
self.max_confetti = 21
25+
26+
# Physics constants
27+
self.GRAVITY = 100 # pixels/sec²
28+
29+
# Screen dimensions
30+
self.screen_width = DisplayMetrics.width()
31+
self.screen_height = DisplayMetrics.height()
32+
33+
# State
34+
self.is_running = False
2635
self.last_time = time.ticks_ms()
27-
28-
# Confetti state
2936
self.confetti_pieces = []
3037
self.confetti_images = []
31-
self.used_img_indices = set() # Track which image slots are in use
32-
38+
self.used_img_indices = set()
39+
self.update_timer = None # Reference to LVGL timer for frame updates
40+
41+
# Spawn control
42+
self.spawn_timer = 0
43+
self.spawn_interval = 0.15 # seconds
44+
self.animation_start = 0
45+
46+
3347
# Pre-create LVGL image objects
34-
for i in range(self.MAX_CONFETTI):
35-
img = lv.image(self.screen)
36-
img.set_src(f"{self.ASSET_PATH}confetti{random.randint(1,3)}.png")
48+
self._init_images()
49+
50+
def _init_images(self):
51+
"""Pre-create LVGL image objects for confetti."""
52+
iconimages = 2
53+
for _ in range(iconimages):
54+
img = lv.image(lv.layer_top())
55+
img.set_src(f"{self.icon_path}icon_64x64.png")
3756
img.add_flag(lv.obj.FLAG.HIDDEN)
3857
self.confetti_images.append(img)
39-
40-
# Spawn initial confetti
41-
for _ in range(self.MAX_CONFETTI):
42-
self.spawn_confetti()
43-
44-
self.setContentView(self.screen)
45-
46-
def onResume(self, screen):
47-
task_handler.add_event_cb(self.update_frame, task_handler.TASK_HANDLER_STARTED)
48-
49-
def onPause(self, screen):
50-
task_handler.remove_event_cb(self.update_frame)
51-
52-
def spawn_confetti(self):
53-
"""Safely spawn a new confetti piece with unique img_idx"""
54-
# Find a free image slot
55-
for idx, img in enumerate(self.confetti_images):
56-
if img.has_flag(lv.obj.FLAG.HIDDEN) and idx not in self.used_img_indices:
57-
break
58-
else:
59-
return # No free slot
60-
61-
piece = {
62-
'img_idx': idx,
63-
'x': random.uniform(-10, self.SCREEN_WIDTH + 10),
64-
'y': random.uniform(50, 150),
65-
'vx': random.uniform(-100, 100),
66-
'vy': random.uniform(-150, -80),
67-
'spin': random.uniform(-400, 400),
68-
'age': 0.0,
69-
'lifetime': random.uniform(1.8, 5),
70-
'rotation': random.uniform(0, 360),
71-
'scale': 1.0
72-
}
73-
self.confetti_pieces.append(piece)
74-
self.used_img_indices.add(idx)
75-
76-
def update_frame(self, a, b):
58+
59+
for i in range(self.max_confetti - iconimages):
60+
img = lv.image(lv.layer_top())
61+
img.set_src(f"{self.asset_path}confetti{random.randint(0, 4)}.png")
62+
img.add_flag(lv.obj.FLAG.HIDDEN)
63+
self.confetti_images.append(img)
64+
65+
def start(self):
66+
"""Start the confetti animation."""
67+
if self.is_running:
68+
return
69+
70+
self.is_running = True
71+
self.last_time = time.ticks_ms()
72+
self._clear_confetti()
73+
74+
# Staggered spawn control
75+
self.spawn_timer = 0
76+
self.animation_start = time.ticks_ms() / 1000.0
77+
78+
# Initial burst
79+
for _ in range(10):
80+
self._spawn_one()
81+
82+
self.update_timer = lv.timer_create(self._update_frame, 16, None) # max 60 fps = 16ms/frame
83+
84+
# Stop spawning after duration
85+
lv.timer_create(self.stop, self.duration, None).set_repeat_count(1)
86+
87+
def stop(self, timer=None):
88+
"""Stop the confetti animation."""
89+
self.is_running = False
90+
91+
def _clear_confetti(self):
92+
"""Clear all confetti pieces from the screen."""
93+
for img in self.confetti_images:
94+
img.add_flag(lv.obj.FLAG.HIDDEN)
95+
self.confetti_pieces = []
96+
self.used_img_indices.clear()
97+
98+
def _update_frame(self, timer):
99+
"""Update frame for confetti animation. Called by LVGL timer."""
77100
current_time = time.ticks_ms()
78-
delta_ms = time.ticks_diff(current_time, self.last_time)
79-
delta_time = delta_ms / 1000.0
101+
delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0
80102
self.last_time = current_time
81-
103+
104+
# === STAGGERED SPAWNING ===
105+
if self.is_running:
106+
self.spawn_timer += delta_time
107+
if self.spawn_timer >= self.spawn_interval:
108+
self.spawn_timer = 0
109+
for _ in range(random.randint(1, 2)):
110+
if len(self.confetti_pieces) < self.max_confetti:
111+
self._spawn_one()
112+
113+
# === UPDATE ALL PIECES ===
82114
new_pieces = []
83-
84115
for piece in self.confetti_pieces:
85-
# === UPDATE PHYSICS ===
116+
# Physics
86117
piece['age'] += delta_time
87118
piece['x'] += piece['vx'] * delta_time
88119
piece['y'] += piece['vy'] * delta_time
89120
piece['vy'] += self.GRAVITY * delta_time
90121
piece['rotation'] += piece['spin'] * delta_time
91122
piece['scale'] = max(0.3, 1.0 - (piece['age'] / piece['lifetime']) * 0.7)
92-
93-
# === UPDATE LVGL IMAGE ===
123+
124+
# Render
94125
img = self.confetti_images[piece['img_idx']]
95126
img.remove_flag(lv.obj.FLAG.HIDDEN)
96127
img.set_pos(int(piece['x']), int(piece['y']))
97-
img.set_rotation(int(piece['rotation'] * 10)) # LVGL: 0.1 degrees
98-
img.set_scale(int(256 * piece['scale']* 2)) # 256 = 100%
99-
100-
# === CHECK IF DEAD ===
101-
off_screen = (
102-
piece['x'] < -60 or piece['x'] > self.SCREEN_WIDTH + 60 or
103-
piece['y'] > self.SCREEN_HEIGHT + 60
128+
img.set_rotation(int(piece['rotation'] * 10))
129+
orig = img.get_width()
130+
if orig >= 64:
131+
img.set_scale(int(256 * piece['scale'] / 1.5))
132+
elif orig < 32:
133+
img.set_scale(int(256 * piece['scale'] * 1.5))
134+
else:
135+
img.set_scale(int(256 * piece['scale']))
136+
137+
# Death check
138+
dead = (
139+
piece['x'] < -60 or piece['x'] > self.screen_width + 60 or
140+
piece['y'] > self.screen_height + 60 or
141+
piece['age'] > piece['lifetime']
104142
)
105-
too_old = piece['age'] > piece['lifetime']
106-
107-
if off_screen or too_old:
143+
144+
if dead:
108145
img.add_flag(lv.obj.FLAG.HIDDEN)
109146
self.used_img_indices.discard(piece['img_idx'])
110-
self.spawn_confetti() # Replace immediately
111147
else:
112148
new_pieces.append(piece)
113-
114-
# === APPLY NEW LIST ===
149+
115150
self.confetti_pieces = new_pieces
151+
152+
# Full stop when empty and paused
153+
if not self.confetti_pieces and not self.is_running:
154+
print("Confetti finished")
155+
if self.update_timer:
156+
self.update_timer.delete()
157+
self.update_timer = None
158+
159+
def _spawn_one(self):
160+
"""Spawn a single confetti piece."""
161+
if not self.is_running:
162+
return
163+
164+
# Find a free image slot
165+
for idx, img in enumerate(self.confetti_images):
166+
if img.has_flag(lv.obj.FLAG.HIDDEN) and idx not in self.used_img_indices:
167+
break
168+
else:
169+
return # No free slot
170+
171+
piece = {
172+
'img_idx': idx,
173+
'x': random.uniform(-50, self.screen_width + 50),
174+
'y': random.uniform(50, 100), # Start above screen
175+
'vx': random.uniform(-80, 80),
176+
'vy': random.uniform(-150, 0),
177+
'spin': random.uniform(-500, 500),
178+
'age': 0.0,
179+
'lifetime': random.uniform(5.0, 10.0), # Long enough to fill 10s
180+
'rotation': random.uniform(0, 360),
181+
'scale': 1.0
182+
}
183+
self.confetti_pieces.append(piece)
184+
self.used_img_indices.add(idx)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import time
2+
import random
3+
import lvgl as lv
4+
5+
from mpos import Activity
6+
7+
from confetti import Confetti
8+
9+
class ConfettiApp(Activity):
10+
11+
ASSET_PATH = "M:apps/com.micropythonos.confetti/res/drawable-mdpi/"
12+
ICON_PATH = "M:apps/com.lightningpiggy.displaywallet/res/mipmap-mdpi/"
13+
confetti_duration = 60 * 1000
14+
15+
confetti = None
16+
17+
def onCreate(self):
18+
main_screen = lv.obj()
19+
self.confetti = Confetti(main_screen, self.ICON_PATH, self.ASSET_PATH, self.confetti_duration)
20+
print("created ", self.confetti)
21+
self.setContentView(main_screen)
22+
23+
def onResume(self, screen):
24+
print("onResume")
25+
self.confetti.start()
26+
27+
def onPause(self, screen):
28+
self.confetti.stop()
5.24 KB
Loading
2.65 KB
Loading

0 commit comments

Comments
 (0)