From 2fffcfe9a33887301f43f7ae965c9aa05b8b27f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:50:59 +0200 Subject: [PATCH] feat(box): celebratory spinning star on bravo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the end-of-routine "Bravo !" lively: the star now spins (continuous full turn via lv_image_set_rotation) and bounces (translate_y ping-pong). Both use layer-free paths — image rotation transforms against the flush band, translate is not a layer-forcing style — so the render task stays light (no repeat of the full-screen-layer task_wdt). Auto-return stretched to 3.6 s so the spin shows and the festive cue finishes. Tower: seed a more festive bravo.mp3 line ("Youpi, tu as réussi ! Tu es super !"). --- main/modes/routine.c | 58 ++++++++++++++++++++++++++++++++++------- tower/lisael_content.py | 2 +- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/main/modes/routine.c b/main/modes/routine.c index a1fe811..7090f34 100644 --- a/main/modes/routine.c +++ b/main/modes/routine.c @@ -385,6 +385,24 @@ static void bravo_auto_return_cb(lv_timer_t *t) bravo_go_home(); } +// 3D-ish spin: rotate the star image about its centre. Uses the IMAGE +// rotation property (lv_image_set_rotation), NOT the style transform_rotation: +// the image path transforms inside the draw against the flush band and does +// not allocate a full-screen object layer (that full-screen layer is what +// wedged the render task with FADE_IN / style transform_scale). The source is +// 44x44 in flash; scaled up ~2.5x for a big celebratory star. +static void bravo_spin_cb(void *var, int32_t v) +{ + lv_image_set_rotation((lv_obj_t *)var, v); // v in 0.1 degree units +} + +// Vertical bounce via translate_y — a pure position offset, layer-free +// (translate is not in LVGL's layer-forcing style set, unlike scale/rotation). +static void bravo_bounce_cb(void *var, int32_t v) +{ + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} + static void show_bravo(void) { lv_obj_t *scr = lv_obj_create(NULL); @@ -392,17 +410,36 @@ static void show_bravo(void) lv_obj_set_style_bg_grad_color(scr, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_bg_grad_dir(scr, LV_GRAD_DIR_VER, 0); - // Star image: pop-in from 0.33x to 1x with overshoot (bouncy feel). + // Spinning, bouncing star (44px source scaled ~2.5x -> ~110px on screen). lv_obj_t *star = lv_image_create(scr); lv_image_set_src(star, &lisael_ic_star); - lv_obj_set_size(star, 140, 140); - lv_image_set_inner_align(star, LV_IMAGE_ALIGN_CONTAIN); + lv_image_set_scale(star, 256 * 5 / 2); // 2.5x + lv_image_set_pivot(star, 22, 22); // centre of the 44px source lv_obj_align(star, LV_ALIGN_CENTER, 0, -40); - // NB: deliberately NO transform/scale animation. A style transform_scale - // forces LVGL to render the star into an intermediate layer; on this - // RAM-tight BOX-3 (16-line flush buffer) that layer allocation wedges the - // render task → task_wdt (the "crash at end of routine"). The screen-load - // slide is the entrance instead. + + // Continuous 3D-feel spin (full turn every 2 s, forever). + lv_anim_t spin; + lv_anim_init(&spin); + lv_anim_set_var(&spin, star); + lv_anim_set_exec_cb(&spin, bravo_spin_cb); + lv_anim_set_values(&spin, 0, 3600); // 0 -> 360.0 deg + lv_anim_set_duration(&spin, 2000); + lv_anim_set_repeat_count(&spin, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&spin); + + // Gentle bounce (down then up, bouncy easing, ping-pong forever). + lv_anim_t bounce; + lv_anim_init(&bounce); + lv_anim_set_var(&bounce, star); + lv_anim_set_exec_cb(&bounce, bravo_bounce_cb); + lv_anim_set_values(&bounce, -18, 14); + lv_anim_set_duration(&bounce, 650); + lv_anim_set_reverse_duration(&bounce, 650); + lv_anim_set_repeat_count(&bounce, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&bounce, lv_anim_path_ease_in_out); + lv_anim_start(&bounce); + // NB: both anims target `star`; LVGL removes them automatically when the + // bravo screen (and its star child) is deleted on return Home. // "Bravo !" label (Fredoka large, centred below the star). lv_obj_t *txt = lv_label_create(scr); @@ -434,7 +471,8 @@ static void show_bravo(void) lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 250, 0, true); s_step_screen = scr; // track the bravo screen for explicit cleanup on return - // Auto-return after 2.2 s (one-shot timer). The tap handler deletes it. - s_bravo_timer = lv_timer_create(bravo_auto_return_cb, 2200, NULL); + // Auto-return after 3.6 s (lets the festive bravo.mp3 ~3.5 s finish and the + // star spin a couple of turns). One-shot; the tap handler deletes it early. + s_bravo_timer = lv_timer_create(bravo_auto_return_cb, 3600, NULL); lv_timer_set_repeat_count(s_bravo_timer, 1); } diff --git a/tower/lisael_content.py b/tower/lisael_content.py index 5438498..51f0196 100644 --- a/tower/lisael_content.py +++ b/tower/lisael_content.py @@ -165,7 +165,7 @@ def build_routine_manifest(gen_bin=None, gen_audio=None): # Seed fixed firmware sound cues (only on the real TTS path, not in test fakes) if _real_audio: _seed_fixed_sound("ok.mp3", "Et hop !") - _seed_fixed_sound("bravo.mp3", "Bravo ! Tu as fini.") + _seed_fixed_sound("bravo.mp3", "Bravo ! Youpi, tu as réussi ! Tu es super !") return {"generated": generated, "skipped": skipped, "routines": manifest} def _gen_step_audio(text, key):