|
|
|
@@ -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;
|
|
|
|
|
}
|