feat(games): tilt-maze game with IMU
New "Labyrinthe" game: guide a ball to the star by tilting the box, read from the on-board ICM42670 accelerometer (BSP I2C bus, added to REQUIRES). A perfect maze is carved by iterative backtracking on a 12x8 grid, so there is one path with branches and dead ends; only a 6x4 window is shown and the view scrolls to follow the ball. The whole world is drawn into a single RGB565 PSRAM canvas (not one object per wall) and scrolled — layer-free, keeps the render task light. The ball bounces off walls and edges with speed-proportional restitution. Tilt axes calibrated on the real board. Wired as the 5th tile in the games carousel; reuses the festive bravo on win.
This commit is contained in:
@@ -28,6 +28,7 @@ set(srcs
|
||||
"modes/clock.c"
|
||||
"modes/games.c"
|
||||
"modes/balloons.c"
|
||||
"modes/maze.c"
|
||||
"modes/histoires.c"
|
||||
"modes/wifi_setup.c"
|
||||
"modes/lire.c"
|
||||
@@ -61,6 +62,7 @@ set(reqs
|
||||
esp_http_server # SoftAP captive provisioning portal
|
||||
mbedtls # esp_crt_bundle_attach (CA bundle for HTTPS)
|
||||
esp-box-3 # BSP: display, touch, codec, sdcard, sensors
|
||||
espressif__icm42670 # IMU accelerometer (tilt maze game)
|
||||
json # IDF built-in cJSON component (provides cJSON.h)
|
||||
fatfs
|
||||
esp_driver_i2c
|
||||
|
||||
+13
-4
@@ -961,12 +961,19 @@ static void balloons_cb(lv_event_t *e)
|
||||
lv_screen_load_anim(lisael_screen_balloons(), LV_SCR_LOAD_ANIM_MOVE_LEFT,
|
||||
250, 0, false);
|
||||
}
|
||||
static void maze_cb(lv_event_t *e)
|
||||
{
|
||||
(void)e;
|
||||
// The maze screen is self-contained (own back button to HOME, IMU tick).
|
||||
lv_screen_load_anim(lisael_screen_maze(), LV_SCR_LOAD_ANIM_MOVE_LEFT,
|
||||
250, 0, false);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Games menu: horizontal tile carousel (mirrors home.c pattern).
|
||||
// ---------------------------------------------------------------------------
|
||||
#define GAMES_SLIDE_W 320
|
||||
#define GAMES_N_TILES 4
|
||||
#define GAMES_N_TILES 5
|
||||
|
||||
typedef struct {
|
||||
const lv_image_dsc_t *icon;
|
||||
@@ -1086,12 +1093,14 @@ lv_obj_t *lisael_screen_games(void)
|
||||
{ &lisael_ic_apple, "Compter", {0}, count_cb },
|
||||
{ &lisael_ic_balloon, "Calme ta col\xc3\xa8re", {0}, balloons_cb },
|
||||
{ &lisael_ic_palette, "Coloriage", {0}, color_cb },
|
||||
{ &lisael_ic_star, "Labyrinthe", {0}, maze_cb },
|
||||
};
|
||||
static const uint32_t colors[GAMES_N_TILES] = {
|
||||
0xFFC9C9, // pink — Memory
|
||||
0xB2F2BB, // green — Compter
|
||||
0xA5D8FF, // blue — Calme ta colère
|
||||
0xFFC9C9, // pink — Memory
|
||||
0xB2F2BB, // green — Compter
|
||||
0xA5D8FF, // blue — Calme ta colère
|
||||
0xFFD8A8, // orange — Coloriage
|
||||
0xD0BFFF, // violet — Labyrinthe
|
||||
};
|
||||
|
||||
// Scrollable track (horizontal, snap-center, no scrollbar).
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
// "Labyrinthe" — scrolling tilt-maze game for Lisael Box.
|
||||
//
|
||||
// Guide a ball to the star by TILTING the box: the on-board IMU (ICM42670,
|
||||
// reached over the BSP I2C bus) gives the board's acceleration; its x/y
|
||||
// components steer the ball. Walls block it; reaching the star wins with the
|
||||
// festive celebration and generates a fresh maze.
|
||||
//
|
||||
// The maze is a real labyrinth (not a single corridor): a perfect maze is
|
||||
// carved on a GW x GH grid by iterative backtracking, so there is exactly one
|
||||
// path between the entrance (top-left cell) and the goal (bottom-right cell),
|
||||
// which means plenty of branches and DEAD ENDS. The grid (12x8) is LARGER than
|
||||
// the screen: only a 6x4 window is shown and the view SCROLLS to follow the
|
||||
// ball, so you cannot see the whole maze at once — that is the difficulty.
|
||||
//
|
||||
// Rendering: the whole world (walls) is drawn ONCE into a single RGB565 canvas
|
||||
// held in PSRAM, NOT one LVGL object per wall (172 objects would blow the 64 KB
|
||||
// LVGL heap and wedge the render task — the failure mode seen elsewhere). The
|
||||
// canvas + the ball + the goal are children of one "world" container that is
|
||||
// scrolled with lv_obj_set_pos; the play-area parent clips it to the viewport.
|
||||
//
|
||||
// TILT DIRECTION calibrated on the real board (tilt right -> ball right, tilt
|
||||
// toward -> ball down). Flip a sign / swap a source axis in MAZE_A*_* if needed.
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/assets/lisael_icons.h"
|
||||
#include "ui/fonts/lisael_fonts.h"
|
||||
#include "audio/aac_player.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
#include "icm42670.h"
|
||||
|
||||
static const char *TAG = "ui.maze";
|
||||
|
||||
// --- tilt -> screen axis mapping (calibrated on HW) --------------------------
|
||||
#define MAZE_AX_SRC x
|
||||
#define MAZE_AX_SIGN (-1.0f)
|
||||
#define MAZE_AY_SRC y
|
||||
#define MAZE_AY_SIGN (-1.0f)
|
||||
|
||||
// --- viewport + grid geometry ------------------------------------------------
|
||||
#define PA_X 10
|
||||
#define PA_Y 44
|
||||
#define PA_W 300 // viewport = 6 cells across
|
||||
#define PA_H 184 // viewport = 4 cells down
|
||||
#define GW 12 // full grid columns
|
||||
#define GH 8 // full grid rows
|
||||
#define CW 50 // cell width (PA_W / 6)
|
||||
#define CH 46 // cell height (PA_H / 4)
|
||||
#define WORLD_W (GW * CW) // 600
|
||||
#define WORLD_H (GH * CH) // 368
|
||||
#define WALL_TH 8
|
||||
#define BALL_R 9
|
||||
#define GOAL_R 18
|
||||
#define MAX_WALLS ((GW - 1) * GH + GW * (GH - 1)) // 88 + 84 = 172
|
||||
|
||||
// physics (pixels, ticks of TICK_MS)
|
||||
#define TICK_MS 33
|
||||
#define ACC_GAIN 1.6f
|
||||
#define FRICTION 0.86f
|
||||
#define VMAX 7.0f
|
||||
#define RESTITUTION 0.55f // bounce energy kept on impact (speed-proportional)
|
||||
#define MIN_BOUNCE 1.2f // below this the ball settles instead of jittering
|
||||
|
||||
// Reflect a velocity component on impact: faster in -> faster bounce out.
|
||||
static inline float reflect_v(float v)
|
||||
{
|
||||
float r = -v * RESTITUTION;
|
||||
return (r > -MIN_BOUNCE && r < MIN_BOUNCE) ? 0.0f : r;
|
||||
}
|
||||
|
||||
#define COL_WALL 0x4ADA // RGB565 of #495ED4 (wall)
|
||||
#define COL_BG 0xFFFF // white floor
|
||||
|
||||
typedef struct { int x, y, w, h; } rect_t;
|
||||
|
||||
static lv_obj_t *s_root;
|
||||
static lv_obj_t *s_area; // viewport (clips the world)
|
||||
static lv_obj_t *s_world; // canvas holding the maze; scrolled by camera
|
||||
static lv_obj_t *s_ball; // child of s_world (world coords)
|
||||
static lv_obj_t *s_goal; // child of s_world (world coords)
|
||||
static uint16_t *s_wbuf; // PSRAM canvas buffer (WORLD_W*WORLD_H)
|
||||
static lv_timer_t *s_tick;
|
||||
static icm42670_handle_t s_imu;
|
||||
static float s_bx, s_by, s_vx, s_vy; // ball centre + velocity (world coords)
|
||||
static bool s_won;
|
||||
|
||||
static rect_t s_walls[MAX_WALLS];
|
||||
static int s_nwalls;
|
||||
static int s_goal_x, s_goal_y;
|
||||
|
||||
// --- maze generation (iterative backtracker -> perfect maze) -----------------
|
||||
static bool s_vwall[GW][GH]; // wall on right edge of cell (x,y)
|
||||
static bool s_hwall[GW][GH]; // wall on bottom edge of cell (x,y)
|
||||
static bool s_visited[GW][GH];
|
||||
|
||||
static void generate_maze(void)
|
||||
{
|
||||
for (int x = 0; x < GW; x++) {
|
||||
for (int y = 0; y < GH; y++) {
|
||||
s_vwall[x][y] = true;
|
||||
s_hwall[x][y] = true;
|
||||
s_visited[x][y] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterative DFS (explicit stack) to avoid deep recursion on the LVGL task.
|
||||
static uint8_t stx[GW * GH], sty[GW * GH];
|
||||
int sp = 0;
|
||||
stx[sp] = 0; sty[sp] = 0; sp++;
|
||||
s_visited[0][0] = true;
|
||||
|
||||
while (sp > 0) {
|
||||
int x = stx[sp - 1], y = sty[sp - 1];
|
||||
// collect unvisited neighbours
|
||||
int nx[4], ny[4], rm[4], n = 0; // rm = which wall to remove
|
||||
if (y > 0 && !s_visited[x][y - 1]) { nx[n]=x; ny[n]=y-1; rm[n]=0; n++; }
|
||||
if (x < GW - 1 && !s_visited[x + 1][y]) { nx[n]=x+1; ny[n]=y; rm[n]=1; n++; }
|
||||
if (y < GH - 1 && !s_visited[x][y + 1]) { nx[n]=x; ny[n]=y+1; rm[n]=2; n++; }
|
||||
if (x > 0 && !s_visited[x - 1][y]) { nx[n]=x-1; ny[n]=y; rm[n]=3; n++; }
|
||||
if (n == 0) { sp--; continue; } // dead end -> backtrack
|
||||
int k = (int)(esp_random() % (uint32_t)n);
|
||||
switch (rm[k]) {
|
||||
case 0: s_hwall[x][y - 1] = false; break; // north
|
||||
case 1: s_vwall[x][y] = false; break; // east
|
||||
case 2: s_hwall[x][y] = false; break; // south
|
||||
case 3: s_vwall[x - 1][y] = false; break; // west
|
||||
}
|
||||
s_visited[nx[k]][ny[k]] = true;
|
||||
stx[sp] = nx[k]; sty[sp] = ny[k]; sp++;
|
||||
}
|
||||
|
||||
// Build the wall rectangle list (outer border handled by the clamp).
|
||||
s_nwalls = 0;
|
||||
for (int x = 0; x < GW - 1; x++)
|
||||
for (int y = 0; y < GH; y++)
|
||||
if (s_vwall[x][y])
|
||||
s_walls[s_nwalls++] = (rect_t){ (x + 1) * CW - WALL_TH / 2, y * CH, WALL_TH, CH };
|
||||
for (int x = 0; x < GW; x++)
|
||||
for (int y = 0; y < GH - 1; y++)
|
||||
if (s_hwall[x][y])
|
||||
s_walls[s_nwalls++] = (rect_t){ x * CW, (y + 1) * CH - WALL_TH / 2, CW, WALL_TH };
|
||||
|
||||
s_goal_x = (GW - 1) * CW + CW / 2;
|
||||
s_goal_y = (GH - 1) * CH + CH / 2;
|
||||
}
|
||||
|
||||
// --- world canvas draw -------------------------------------------------------
|
||||
static void fill_rect565(int rx, int ry, int rw, int rh, uint16_t c)
|
||||
{
|
||||
for (int y = ry; y < ry + rh; y++) {
|
||||
if (y < 0 || y >= WORLD_H) continue;
|
||||
uint16_t *row = s_wbuf + (size_t)y * WORLD_W;
|
||||
for (int x = rx; x < rx + rw; x++) {
|
||||
if (x < 0 || x >= WORLD_W) continue;
|
||||
row[x] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_world(void)
|
||||
{
|
||||
for (size_t i = 0; i < (size_t)WORLD_W * WORLD_H; i++) s_wbuf[i] = COL_BG;
|
||||
for (int i = 0; i < s_nwalls; i++) {
|
||||
fill_rect565(s_walls[i].x, s_walls[i].y, s_walls[i].w, s_walls[i].h, COL_WALL);
|
||||
}
|
||||
lv_obj_invalidate(s_world);
|
||||
}
|
||||
|
||||
// --- IMU ---------------------------------------------------------------------
|
||||
static bool imu_init(void)
|
||||
{
|
||||
if (s_imu) {
|
||||
return true;
|
||||
}
|
||||
i2c_master_bus_handle_t bus = bsp_i2c_get_handle();
|
||||
if (!bus) {
|
||||
return false;
|
||||
}
|
||||
if (icm42670_create(bus, ICM42670_I2C_ADDRESS, &s_imu) != ESP_OK || !s_imu) {
|
||||
s_imu = NULL;
|
||||
return false;
|
||||
}
|
||||
icm42670_cfg_t cfg = {
|
||||
.acce_fs = ACCE_FS_2G, .acce_odr = ACCE_ODR_100HZ,
|
||||
.gyro_fs = GYRO_FS_250DPS, .gyro_odr = GYRO_ODR_100HZ,
|
||||
};
|
||||
icm42670_config(s_imu, &cfg);
|
||||
icm42670_acce_set_pwr(s_imu, ACCE_PWR_LOWNOISE);
|
||||
ESP_LOGI(TAG, "IMU ready");
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- celebratory toast (spinning star + sound) -------------------------------
|
||||
static void cel_spin_cb(void *var, int32_t v) { lv_image_set_rotation((lv_obj_t *)var, v); }
|
||||
static void cel_bounce_cb(void *var, int32_t v) { lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); }
|
||||
|
||||
static void celebrate(lv_obj_t *parent)
|
||||
{
|
||||
lv_obj_t *toast = lv_obj_create(parent);
|
||||
lv_obj_set_size(toast, 220, 96);
|
||||
lv_obj_center(toast);
|
||||
lv_obj_set_style_bg_color(toast, lv_color_hex(0xFFF3BF), 0);
|
||||
lv_obj_set_style_radius(toast, 16, 0);
|
||||
lv_obj_set_flex_flow(toast, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(toast, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(toast, 10, 0);
|
||||
lv_obj_clear_flag(toast, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t *lbl = lv_label_create(toast);
|
||||
lv_label_set_text(lbl, "Bravo !");
|
||||
lv_obj_set_style_text_font(lbl, &lisael_font_24, 0);
|
||||
|
||||
lv_obj_t *star = lv_image_create(toast);
|
||||
lv_image_set_src(star, &lisael_ic_star);
|
||||
lv_image_set_scale(star, 256 * 5 / 4);
|
||||
lv_image_set_pivot(star, 22, 22);
|
||||
|
||||
lv_anim_t spin;
|
||||
lv_anim_init(&spin);
|
||||
lv_anim_set_var(&spin, star);
|
||||
lv_anim_set_exec_cb(&spin, cel_spin_cb);
|
||||
lv_anim_set_values(&spin, 0, 3600);
|
||||
lv_anim_set_duration(&spin, 1400);
|
||||
lv_anim_set_repeat_count(&spin, LV_ANIM_REPEAT_INFINITE);
|
||||
lv_anim_start(&spin);
|
||||
|
||||
lv_anim_t bounce;
|
||||
lv_anim_init(&bounce);
|
||||
lv_anim_set_var(&bounce, star);
|
||||
lv_anim_set_exec_cb(&bounce, cel_bounce_cb);
|
||||
lv_anim_set_values(&bounce, -10, 10);
|
||||
lv_anim_set_duration(&bounce, 500);
|
||||
lv_anim_set_reverse_duration(&bounce, 500);
|
||||
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);
|
||||
|
||||
lisael_aac_play_file("/sdcard/routines/bravo.mp3");
|
||||
lv_obj_delete_delayed(toast, 2200);
|
||||
}
|
||||
|
||||
// --- camera (scroll the world so the ball stays centred, clamped) ------------
|
||||
static void update_camera(void)
|
||||
{
|
||||
int camx = (int)s_bx - PA_W / 2;
|
||||
int camy = (int)s_by - PA_H / 2;
|
||||
if (camx < 0) camx = 0;
|
||||
if (camx > WORLD_W - PA_W) camx = WORLD_W - PA_W;
|
||||
if (camy < 0) camy = 0;
|
||||
if (camy > WORLD_H - PA_H) camy = WORLD_H - PA_H;
|
||||
lv_obj_set_pos(s_world, -camx, -camy);
|
||||
}
|
||||
|
||||
// --- maze build / load -------------------------------------------------------
|
||||
static void load_maze(void)
|
||||
{
|
||||
generate_maze();
|
||||
draw_world();
|
||||
|
||||
lv_obj_set_pos(s_goal, s_goal_x - 16, s_goal_y - 16); // world coords
|
||||
|
||||
s_bx = CW / 2; s_by = CH / 2; // start in the top-left cell
|
||||
s_vx = s_vy = 0.0f;
|
||||
lv_obj_set_pos(s_ball, (int)s_bx - BALL_R, (int)s_by - BALL_R);
|
||||
update_camera();
|
||||
s_won = false;
|
||||
}
|
||||
|
||||
static float collide_axis(float pos, float other, bool is_x, float *vel)
|
||||
{
|
||||
float bx = is_x ? pos : other;
|
||||
float by = is_x ? other : pos;
|
||||
for (int i = 0; i < s_nwalls; i++) {
|
||||
const rect_t *w = &s_walls[i];
|
||||
float bl = bx - BALL_R, br = bx + BALL_R, bt = by - BALL_R, bb = by + BALL_R;
|
||||
if (br > w->x && bl < w->x + w->w && bb > w->y && bt < w->y + w->h) {
|
||||
if (is_x) {
|
||||
if (*vel > 0) pos = w->x - BALL_R;
|
||||
else if (*vel < 0) pos = w->x + w->w + BALL_R;
|
||||
} else {
|
||||
if (*vel > 0) pos = w->y - BALL_R;
|
||||
else if (*vel < 0) pos = w->y + w->h + BALL_R;
|
||||
}
|
||||
*vel = reflect_v(*vel); // bounce, scaled by impact speed
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
static void next_maze_cb(lv_timer_t *t); // fwd decl
|
||||
|
||||
static void tick_cb(lv_timer_t *t)
|
||||
{
|
||||
(void)t;
|
||||
if (s_won || !s_imu) {
|
||||
return;
|
||||
}
|
||||
icm42670_value_t a;
|
||||
if (icm42670_get_acce_value(s_imu, &a) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
float ax = MAZE_AX_SIGN * a.MAZE_AX_SRC;
|
||||
float ay = MAZE_AY_SIGN * a.MAZE_AY_SRC;
|
||||
|
||||
s_vx = (s_vx + ax * ACC_GAIN) * FRICTION;
|
||||
s_vy = (s_vy + ay * ACC_GAIN) * FRICTION;
|
||||
if (s_vx > VMAX) s_vx = VMAX; else if (s_vx < -VMAX) s_vx = -VMAX;
|
||||
if (s_vy > VMAX) s_vy = VMAX; else if (s_vy < -VMAX) s_vy = -VMAX;
|
||||
|
||||
s_bx += s_vx;
|
||||
if (s_bx < BALL_R) { s_bx = BALL_R; s_vx = reflect_v(s_vx); }
|
||||
if (s_bx > WORLD_W - BALL_R) { s_bx = WORLD_W - BALL_R; s_vx = reflect_v(s_vx); }
|
||||
s_bx = collide_axis(s_bx, s_by, true, &s_vx);
|
||||
|
||||
s_by += s_vy;
|
||||
if (s_by < BALL_R) { s_by = BALL_R; s_vy = reflect_v(s_vy); }
|
||||
if (s_by > WORLD_H - BALL_R) { s_by = WORLD_H - BALL_R; s_vy = reflect_v(s_vy); }
|
||||
s_by = collide_axis(s_by, s_bx, false, &s_vy);
|
||||
|
||||
lv_obj_set_pos(s_ball, (int)s_bx - BALL_R, (int)s_by - BALL_R);
|
||||
update_camera();
|
||||
|
||||
float dx = s_bx - s_goal_x, dy = s_by - s_goal_y;
|
||||
if (dx * dx + dy * dy <= (float)(GOAL_R * GOAL_R)) {
|
||||
s_won = true;
|
||||
ESP_LOGI(TAG, "maze solved");
|
||||
celebrate(s_root);
|
||||
lv_timer_t *adv = lv_timer_create(next_maze_cb, 2400, NULL);
|
||||
lv_timer_set_repeat_count(adv, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void next_maze_cb(lv_timer_t *t)
|
||||
{
|
||||
load_maze();
|
||||
lv_timer_delete(t);
|
||||
}
|
||||
|
||||
static void back_cb(lv_event_t *e)
|
||||
{
|
||||
(void)e;
|
||||
if (s_tick) lv_timer_pause(s_tick);
|
||||
lisael_ui_goto(LISAEL_MODE_HOME);
|
||||
}
|
||||
|
||||
lv_obj_t *lisael_screen_maze(void)
|
||||
{
|
||||
if (s_root) {
|
||||
if (s_tick) lv_timer_resume(s_tick);
|
||||
load_maze(); // fresh maze on re-entry
|
||||
return s_root;
|
||||
}
|
||||
|
||||
s_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xEAF3FF), 0);
|
||||
lv_obj_set_style_bg_grad_color(s_root, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_set_style_bg_grad_dir(s_root, LV_GRAD_DIR_VER, 0);
|
||||
|
||||
lv_obj_t *back = lv_button_create(s_root);
|
||||
lv_obj_add_style(back, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_size(back, 64, 56);
|
||||
lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6);
|
||||
lv_obj_add_event_cb(back, back_cb, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_t *bl = lv_label_create(back);
|
||||
lv_label_set_text(bl, LV_SYMBOL_LEFT);
|
||||
lv_obj_center(bl);
|
||||
|
||||
lv_obj_t *title = lv_label_create(s_root);
|
||||
lv_label_set_text(title, "Penche pour guider");
|
||||
lv_obj_set_style_text_font(title, &lisael_font_18, 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16);
|
||||
|
||||
// Viewport: bordered, clips the (larger) scrolling world.
|
||||
s_area = lv_obj_create(s_root);
|
||||
lv_obj_remove_style_all(s_area);
|
||||
lv_obj_set_pos(s_area, PA_X, PA_Y);
|
||||
lv_obj_set_size(s_area, PA_W, PA_H);
|
||||
lv_obj_set_style_radius(s_area, 10, 0);
|
||||
lv_obj_set_style_border_width(s_area, 3, 0);
|
||||
lv_obj_set_style_border_color(s_area, lv_color_hex(0x495ED4), 0);
|
||||
lv_obj_set_style_clip_corner(s_area, true, 0);
|
||||
lv_obj_clear_flag(s_area, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// World canvas (the whole maze), child of the viewport.
|
||||
s_wbuf = heap_caps_malloc((size_t)WORLD_W * WORLD_H * sizeof(uint16_t),
|
||||
MALLOC_CAP_SPIRAM);
|
||||
s_world = lv_canvas_create(s_area);
|
||||
if (s_wbuf) {
|
||||
lv_canvas_set_buffer(s_world, s_wbuf, WORLD_W, WORLD_H, LV_COLOR_FORMAT_RGB565);
|
||||
}
|
||||
lv_obj_set_pos(s_world, 0, 0);
|
||||
|
||||
// Goal star + ball are children of the world (so they scroll with it).
|
||||
s_goal = lv_image_create(s_world);
|
||||
lv_image_set_src(s_goal, &lisael_ic_star);
|
||||
lv_obj_set_size(s_goal, 32, 32);
|
||||
lv_image_set_inner_align(s_goal, LV_IMAGE_ALIGN_CONTAIN);
|
||||
|
||||
s_ball = lv_obj_create(s_world);
|
||||
lv_obj_remove_style_all(s_ball);
|
||||
lv_obj_set_size(s_ball, BALL_R * 2, BALL_R * 2);
|
||||
lv_obj_set_style_radius(s_ball, BALL_R, 0);
|
||||
lv_obj_set_style_bg_opa(s_ball, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(s_ball, lv_color_hex(0xE8590C), 0);
|
||||
lv_obj_set_style_shadow_width(s_ball, 6, 0);
|
||||
lv_obj_set_style_shadow_color(s_ball, lv_color_hex(0x00000040), 0);
|
||||
|
||||
if (!imu_init() || !s_wbuf) {
|
||||
lv_obj_t *err = lv_label_create(s_root);
|
||||
lv_label_set_text(err, "Capteur indisponible");
|
||||
lv_obj_set_style_text_align(err, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_align(err, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
}
|
||||
|
||||
load_maze();
|
||||
s_tick = lv_timer_create(tick_cb, TICK_MS, NULL);
|
||||
return s_root;
|
||||
}
|
||||
@@ -51,6 +51,7 @@ lv_obj_t *lisael_screen_calm(void);
|
||||
lv_obj_t *lisael_screen_clock(void);
|
||||
lv_obj_t *lisael_screen_games(void);
|
||||
lv_obj_t *lisael_screen_balloons(void); // "Calme ta colère" anger game
|
||||
lv_obj_t *lisael_screen_maze(void); // "Labyrinthe" — tilt-maze (IMU)
|
||||
lv_obj_t *lisael_screen_histoires(void); // "Histoires" — vignette podcasts
|
||||
lv_obj_t *lisael_screen_wifi_setup(void); // SoftAP provisioning info screen
|
||||
lv_obj_t *lisael_screen_lire(void); // "Lire" learn-to-read game
|
||||
|
||||
Reference in New Issue
Block a user