From 9e52aab213ba8552a482134c0071242499298b55 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 19 Jan 2026 23:01:48 -0600 Subject: [PATCH 01/21] feat(expressive_eyes): Added new `ExpressiveEyes` component --- components/expressive_eyes/CMakeLists.txt | 5 + components/expressive_eyes/README.md | 86 +++++ .../expressive_eyes/example/CMakeLists.txt | 22 ++ components/expressive_eyes/example/README.md | 82 ++++ .../example/main/CMakeLists.txt | 2 + .../example/main/Kconfig.projbuild | 99 +++++ .../example/main/expressive_eyes_example.cpp | 358 ++++++++++++++++++ .../example/sdkconfig.defaults | 35 ++ components/expressive_eyes/idf_component.yml | 17 + .../include/expressive_eyes.hpp | 236 ++++++++++++ .../expressive_eyes/src/expressive_eyes.cpp | 318 ++++++++++++++++ 11 files changed, 1260 insertions(+) create mode 100644 components/expressive_eyes/CMakeLists.txt create mode 100644 components/expressive_eyes/README.md create mode 100644 components/expressive_eyes/example/CMakeLists.txt create mode 100644 components/expressive_eyes/example/README.md create mode 100644 components/expressive_eyes/example/main/CMakeLists.txt create mode 100644 components/expressive_eyes/example/main/Kconfig.projbuild create mode 100644 components/expressive_eyes/example/main/expressive_eyes_example.cpp create mode 100644 components/expressive_eyes/example/sdkconfig.defaults create mode 100644 components/expressive_eyes/idf_component.yml create mode 100644 components/expressive_eyes/include/expressive_eyes.hpp create mode 100644 components/expressive_eyes/src/expressive_eyes.cpp diff --git a/components/expressive_eyes/CMakeLists.txt b/components/expressive_eyes/CMakeLists.txt new file mode 100644 index 000000000..ca7cc6e15 --- /dev/null +++ b/components/expressive_eyes/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "src/expressive_eyes.cpp" + INCLUDE_DIRS "include" + REQUIRES logger base_component +) diff --git a/components/expressive_eyes/README.md b/components/expressive_eyes/README.md new file mode 100644 index 000000000..2d8ced90b --- /dev/null +++ b/components/expressive_eyes/README.md @@ -0,0 +1,86 @@ +# Expressive Eyes Component + +Animated expressive eyes for displays using simple blob shapes. + +## Features + +- Multiple expressions (happy, sad, angry, surprised, sleepy, wink) +- Smooth eye movement and blinking +- Physics-based pupil movement +- Customizable appearance +- Frame-based animation system +- Automatic random blinking + +## Usage + +```cpp +//! [expressive eyes example] +#include "expressive_eyes.hpp" +#include "display.hpp" // Your display driver + +// Drawing callback +auto draw_eye = [](int x, int y, int width, int height, float rotation, + uint16_t color, bool has_pupil, float pupil_x, float pupil_y) { + // Draw eye ellipse + display.fill_ellipse(x, y, width/2, height/2, color); + + if (has_pupil && height > 10) { + // Calculate pupil position + int px = x + static_cast(pupil_x * width * 0.3f); + int py = y + static_cast(pupil_y * height * 0.3f); + display.fill_circle(px, py, 10, 0x0000); // Black pupil + } +}; + +// Configure eyes +espp::ExpressiveEyes::Config config{ + .screen_width = 320, + .screen_height = 240, + .eye_spacing = 100, + .eye_width = 60, + .eye_height = 80, + .pupil_size = 20, + .enable_auto_blink = true, + .enable_pupil_physics = true, + .on_draw = draw_eye +}; + +espp::ExpressiveEyes eyes(config); + +// Animation loop +auto last_time = std::chrono::steady_clock::now(); +while (true) { + auto now = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(now - last_time).count(); + last_time = now; + + eyes.update(dt); + + // Change expression based on input + eyes.set_expression(espp::ExpressiveEyes::Expression::HAPPY); + + // Look at position + eyes.look_at(0.5f, -0.3f); // Look up-right + + std::this_thread::sleep_for(16ms); // ~60 FPS +} +//! [expressive eyes example] +``` + +## Expressions + +- `NEUTRAL` - Normal open eyes +- `HAPPY` - Squinted happy eyes +- `SAD` - Droopy sad eyes +- `ANGRY` - Angled angry eyebrows +- `SURPRISED` - Wide open eyes +- `SLEEPY` - Half-closed eyes +- `WINK_LEFT` - Left eye closed +- `WINK_RIGHT` - Right eye closed + +## Customization + +- Adjust `eye_width` and `eye_height` for different eye shapes +- Set `pupil_size = 0` to disable pupils +- Modify `blink_interval` to change blink frequency +- Disable `enable_pupil_physics` for instant pupil movement diff --git a/components/expressive_eyes/example/CMakeLists.txt b/components/expressive_eyes/example/CMakeLists.txt new file mode 100644 index 000000000..0c211e422 --- /dev/null +++ b/components/expressive_eyes/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py expressive_eyes display task box box-3 esp-box esp-box-3" + CACHE STRING + "List of components to include" + ) + +project(expressive_eyes_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/expressive_eyes/example/README.md b/components/expressive_eyes/example/README.md new file mode 100644 index 000000000..a0af1e80d --- /dev/null +++ b/components/expressive_eyes/example/README.md @@ -0,0 +1,82 @@ +# Expressive Eyes Example + +Demonstrates animated expressive eyes on various display boards. + +## Hardware Required + +Select one of the supported boards in menuconfig: +- ESP32-S3-BOX / ESP32-S3-BOX-3 +- MaTouch Rotary Display +- ESP-WROVER-KIT +- M5Stack Tab5 + +## How to Use + +### Hardware Setup + +Connect your chosen display board. No additional wiring is needed. + +### Configure the Project + +```bash +idf.py menuconfig +``` + +Navigate to `Expressive Eyes Example Configuration`: +- Select your board +- Configure eye appearance (size, spacing, color) +- Enable/disable auto demo mode +- Configure blink settings +- Enable/disable pupils + +### Build and Flash + +```bash +idf.py build flash monitor +``` + +## Features + +### Auto Demo Mode (Enabled by default) + +Automatically cycles through different expressions every 3 seconds (configurable): +- Neutral +- Happy +- Sad +- Angry +- Surprised +- Sleepy +- Wink Left +- Wink Right + +Eyes also randomly look around and blink automatically. + +### Manual Mode + +Disable auto demo to show a single expression. Modify the code to control expressions programmatically. + +### Customization + +All parameters can be adjusted via menuconfig without code changes: +- Eye dimensions and spacing +- Pupil size (or disable pupils entirely) +- Blink frequency +- Demo interval +- Eye color (RGB565) + +## Example Output + +``` +I (380) Expressive Eyes Example: Starting Expressive Eyes Example +I (425) Expressive Eyes Example: Display size: 320x240 +I (430) Expressive Eyes Example: Expressive eyes initialized +I (435) Expressive Eyes Example: Starting auto demo mode (interval: 3000ms) +I (3440) Expressive Eyes Example: Changed to expression 1, looking at (0.42, -0.67) +I (6445) Expressive Eyes Example: Changed to expression 2, looking at (-0.23, 0.81) +``` + +## Troubleshooting + +- **Display not working**: Check board selection in menuconfig +- **Eyes too small/large**: Adjust eye width/height in menuconfig +- **Performance issues**: Try reducing eye size or disabling pupils diff --git a/components/expressive_eyes/example/main/CMakeLists.txt b/components/expressive_eyes/example/main/CMakeLists.txt new file mode 100644 index 000000000..a941e22ba --- /dev/null +++ b/components/expressive_eyes/example/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS ".") diff --git a/components/expressive_eyes/example/main/Kconfig.projbuild b/components/expressive_eyes/example/main/Kconfig.projbuild new file mode 100644 index 000000000..f2d15a458 --- /dev/null +++ b/components/expressive_eyes/example/main/Kconfig.projbuild @@ -0,0 +1,99 @@ +menu "Expressive Eyes Example Configuration" + + choice EXPRESSIVE_EYES_BOARD + prompt "Select Board" + default EXPRESSIVE_EYES_BOARD_ESP_BOX + help + Select the hardware board to run the example on. + + config EXPRESSIVE_EYES_BOARD_ESP_BOX + bool "ESP-BOX" + help + Use ESP32-S3-BOX or ESP32-S3-BOX-3 development board. + + config EXPRESSIVE_EYES_BOARD_MATOUCH_ROTARY + bool "MaTouch Rotary Display" + help + Use MaTouch Rotary Display board. + + config EXPRESSIVE_EYES_BOARD_WROVER_KIT + bool "ESP-WROVER-KIT" + help + Use ESP-WROVER-KIT development board. + + config EXPRESSIVE_EYES_BOARD_TAB5 + bool "M5Stack Tab5" + help + Use M5Stack Tab5 development board. + + endchoice + + config EXPRESSIVE_EYES_AUTO_DEMO + bool "Enable Auto Demo Mode" + default y + help + Automatically cycle through different expressions and eye movements. + + config EXPRESSIVE_EYES_DEMO_INTERVAL_MS + int "Demo Interval (ms)" + default 3000 + depends on EXPRESSIVE_EYES_AUTO_DEMO + help + Time in milliseconds between expression changes in demo mode. + + config EXPRESSIVE_EYES_ENABLE_PUPILS + bool "Enable Pupils" + default y + help + Draw pupils in the eyes. + + config EXPRESSIVE_EYES_PUPIL_SIZE + int "Pupil Size" + default 15 + depends on EXPRESSIVE_EYES_ENABLE_PUPILS + range 5 50 + help + Size of the pupils in pixels. + + config EXPRESSIVE_EYES_AUTO_BLINK + bool "Enable Auto Blink" + default y + help + Automatically blink the eyes at random intervals. + + config EXPRESSIVE_EYES_BLINK_INTERVAL_MS + int "Average Blink Interval (ms)" + default 4000 + depends on EXPRESSIVE_EYES_AUTO_BLINK + range 1000 10000 + help + Average time between blinks in milliseconds. + + config EXPRESSIVE_EYES_EYE_SPACING + int "Eye Spacing" + default 100 + range 20 300 + help + Distance between the centers of the two eyes in pixels. + + config EXPRESSIVE_EYES_EYE_WIDTH + int "Eye Width" + default 60 + range 20 200 + help + Width of each eye in pixels. + + config EXPRESSIVE_EYES_EYE_HEIGHT + int "Eye Height" + default 80 + range 20 200 + help + Height of each eye in pixels. + + config EXPRESSIVE_EYES_COLOR + hex "Eye Color (RGB565)" + default 0xFFFF + help + Color of the eyes in RGB565 format (default: white 0xFFFF). + +endmenu diff --git a/components/expressive_eyes/example/main/expressive_eyes_example.cpp b/components/expressive_eyes/example/main/expressive_eyes_example.cpp new file mode 100644 index 000000000..5c0df4ee7 --- /dev/null +++ b/components/expressive_eyes/example/main/expressive_eyes_example.cpp @@ -0,0 +1,358 @@ +#include +#include +#include +#include + +#include "expressive_eyes.hpp" +#include "logger.hpp" +#include "task.hpp" + +// Board-specific includes based on menuconfig selection +#if CONFIG_EXPRESSIVE_EYES_BOARD_ESP_BOX +#include "esp-box.hpp" +using Board = espp::EspBox; +#elif CONFIG_EXPRESSIVE_EYES_BOARD_MATOUCH_ROTARY +#include "matouch-rotary-display.hpp" +using Board = espp::MatouchRotaryDisplay; +#elif CONFIG_EXPRESSIVE_EYES_BOARD_WROVER_KIT +#include "wrover-kit.hpp" +using Board = espp::WroverKit; +#elif CONFIG_EXPRESSIVE_EYES_BOARD_TAB5 +#include "m5stack-tab5.hpp" +using Board = espp::M5StackTab5; +#else +#error "No board selected in menuconfig!" +#endif + +using namespace std::chrono_literals; + +static std::recursive_mutex lvgl_mutex; + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "Expressive Eyes Example", .level = espp::Logger::Verbosity::INFO}); + + logger.info("Starting Expressive Eyes Example"); + + //! [expressive eyes example] + // Initialize the board + Board &board = Board::get(); + board.set_log_level(espp::Logger::Verbosity::INFO); + + // Initialize the LCD + if (!board.initialize_lcd()) { + logger.error("Failed to initialize LCD!"); + return; + } + + // Set up pixel buffer (50 lines) + static constexpr size_t pixel_buffer_size = board.lcd_width() * 50; + + // Initialize the display + if (!board.initialize_display(pixel_buffer_size)) { + logger.error("Failed to initialize display!"); + return; + } + + // Get screen dimensions + int screen_width = board.lcd_width(); + int screen_height = board.lcd_height(); + + logger.info("Display size: {}x{}", screen_width, screen_height); + + // Disable scrollbars on screen + lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); + + // Get background color + lv_color_t bg_color = lv_obj_get_style_bg_color(lv_screen_active(), LV_PART_MAIN); + + // Create main canvas for drawing everything + lv_obj_t *canvas = lv_canvas_create(lv_screen_active()); + + // Allocate buffer for main canvas (RGB565 = 2 bytes per pixel) + static lv_color_t *canvas_buffer = nullptr; + canvas_buffer = (lv_color_t *)heap_caps_malloc(screen_width * screen_height * sizeof(lv_color_t), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!canvas_buffer) { + logger.error("Failed to allocate canvas buffer!"); + return; + } + + lv_canvas_set_buffer(canvas, canvas_buffer, screen_width, screen_height, LV_COLOR_FORMAT_RGB565); + lv_obj_center(canvas); + + // Store original eye dimensions for calculations + int original_eye_height = screen_height * 0.55f; + int eye_base_width = screen_width * 0.35f; + int pupil_size = static_cast(std::min(eye_base_width, original_eye_height) * 0.3f); + + // Drawing callback for eyes - single call for both eyes using canvas + auto draw_eyes = [&](const espp::ExpressiveEyes::EyeState &left, + const espp::ExpressiveEyes::EyeState &right) { + std::lock_guard lock(lvgl_mutex); + + // Clear canvas with background color + lv_canvas_fill_bg(canvas, bg_color, LV_OPA_COVER); + + // Helper to draw filled ellipse (for eyes) using layer API + auto draw_ellipse = [&](int cx, int cy, int width, int height, lv_color_t color) { + lv_layer_t layer; + lv_canvas_init_layer(canvas, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = color; + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + area.x1 = cx - width / 2; + area.y1 = cy - height / 2; + area.x2 = cx + width / 2; + area.y2 = cy + height / 2; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas, &layer); + }; + + // Lambda to draw single eye + auto draw_single_eye = [&](const espp::ExpressiveEyes::EyeState &eye_state, bool is_left) { + // Draw eye white + draw_ellipse(eye_state.x, eye_state.y, eye_state.width, eye_state.height, lv_color_white()); + + // Draw pupil (use original height so blink doesn't move pupil) + // Only draw if eye is open enough (not blinking) + float openness = eye_state.height / static_cast(original_eye_height); + if (eye_state.expression.pupil.enabled && openness > 0.3f) { + int px_offset = static_cast(eye_state.expression.pupil.x * eye_state.width * 0.3f); + int py_offset = static_cast(eye_state.expression.pupil.y * original_eye_height * 0.3f); + int pupil_x = eye_state.x + px_offset; + int pupil_y = eye_state.y + py_offset; + + lv_layer_t layer; + lv_canvas_init_layer(canvas, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = lv_color_black(); + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + int r = pupil_size / 2; + area.x1 = pupil_x - r; + area.y1 = pupil_y - r; + area.x2 = pupil_x + r; + area.y2 = pupil_y + r; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas, &layer); + } + + // Draw eyebrow as rotated rectangle (using triangles) + if (eye_state.expression.eyebrow.enabled) { + int brow_width = + static_cast(eye_base_width * eye_state.expression.eyebrow.width * 1.5f); + int brow_height = + static_cast(eye_base_width * eye_state.expression.eyebrow.thickness * 4.0f); + int brow_y = eye_state.y - static_cast(original_eye_height * 0.4f); + + // For left eye, positive angle tilts left side down (clockwise rotation) + // For right eye, positive angle tilts right side down (counter-clockwise rotation) + float angle_rad = eye_state.expression.eyebrow.angle * M_PI / 180.0f; + if (!is_left) + angle_rad = -angle_rad; // Mirror for right eye + + // Calculate the 4 corners of a rotated rectangle centered at (eye_state.x, brow_y) + float half_w = brow_width / 2.0f; + float half_h = brow_height / 2.0f; + float cos_a = std::cos(angle_rad); + float sin_a = std::sin(angle_rad); + + // Four corners relative to center, then rotated + int x1 = eye_state.x + static_cast(-half_w * cos_a + half_h * sin_a); + int y1 = brow_y + static_cast(-half_w * sin_a - half_h * cos_a); + + int x2 = eye_state.x + static_cast(half_w * cos_a + half_h * sin_a); + int y2 = brow_y + static_cast(half_w * sin_a - half_h * cos_a); + + int x3 = eye_state.x + static_cast(half_w * cos_a - half_h * sin_a); + int y3 = brow_y + static_cast(half_w * sin_a + half_h * cos_a); + + int x4 = eye_state.x + static_cast(-half_w * cos_a - half_h * sin_a); + int y4 = brow_y + static_cast(-half_w * sin_a + half_h * cos_a); + + lv_layer_t layer; + lv_canvas_init_layer(canvas, &layer); + + lv_draw_triangle_dsc_t tri_dsc; + lv_draw_triangle_dsc_init(&tri_dsc); + tri_dsc.color = bg_color; + tri_dsc.opa = LV_OPA_COVER; + + // First triangle: top-left, top-right, bottom-right + tri_dsc.p[0].x = x1; + tri_dsc.p[0].y = y1; + tri_dsc.p[1].x = x2; + tri_dsc.p[1].y = y2; + tri_dsc.p[2].x = x3; + tri_dsc.p[2].y = y3; + lv_draw_triangle(&layer, &tri_dsc); + + // Second triangle: top-left, bottom-right, bottom-left + tri_dsc.p[0].x = x1; + tri_dsc.p[0].y = y1; + tri_dsc.p[1].x = x3; + tri_dsc.p[1].y = y3; + tri_dsc.p[2].x = x4; + tri_dsc.p[2].y = y4; + lv_draw_triangle(&layer, &tri_dsc); + + lv_canvas_finish_layer(canvas, &layer); + } + + // Draw cheek + if (eye_state.expression.cheek.enabled) { + int cheek_width = static_cast(eye_base_width * eye_state.expression.cheek.size * 2.5f); + int cheek_height = + static_cast(eye_base_width * eye_state.expression.cheek.size * 1.5f); + int cheek_y = eye_state.y + static_cast(original_eye_height * 0.4f) + + static_cast(eye_state.expression.cheek_offset_y * screen_height); + + lv_layer_t layer; + lv_canvas_init_layer(canvas, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = bg_color; + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + area.x1 = eye_state.x - cheek_width / 2; + area.y1 = cheek_y - cheek_height / 2; + area.x2 = eye_state.x + cheek_width / 2; + area.y2 = cheek_y + cheek_height / 2; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas, &layer); + } + }; + + // Draw both eyes + draw_single_eye(left, true); + draw_single_eye(right, false); + }; + + // Configure expressive eyes with adaptive sizing - make eyes larger + int large_eye_width = screen_width * 0.35f; // 35% of screen width each + int large_eye_height = screen_height * 0.55f; // 55% of screen height + int large_spacing = screen_width * 0.55f; // Space between eye centers + + espp::ExpressiveEyes::Config config{.screen_width = screen_width, + .screen_height = screen_height, + .eye_spacing = large_spacing, + .eye_width = large_eye_width, + .eye_height = large_eye_height, + .eye_color = 0xFFFF, + .blink_duration = 0.12f, + .blink_interval = 4.0f, + .enable_auto_blink = true, + .enable_pupil_physics = true, + .on_draw = draw_eyes, + .log_level = espp::Logger::Verbosity::WARN}; + + espp::ExpressiveEyes eyes(config); + + // start a simple thread to do the lv_task_handler every 16ms + espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool { + { + std::lock_guard lock(lvgl_mutex); + lv_task_handler(); + } + std::unique_lock lock(m); + cv.wait_for(lock, 16ms); + return false; + }, + .task_config = { + .name = "lv_task", + .stack_size_bytes = 16 * 1024, + }}); + lv_task.start(); + + logger.info("Expressive eyes initialized"); + + // Test different expressions using array iteration + logger.info("Testing different expressions..."); + + const espp::ExpressiveEyes::Expression expressions[] = { + espp::ExpressiveEyes::Expression::NEUTRAL, espp::ExpressiveEyes::Expression::HAPPY, + espp::ExpressiveEyes::Expression::SAD, espp::ExpressiveEyes::Expression::ANGRY, + espp::ExpressiveEyes::Expression::SURPRISED}; + + auto last_time = std::chrono::steady_clock::now(); + for (const auto &expr : expressions) { + logger.info("Expression: {}", espp::ExpressiveEyes::expression_name(expr)); + eyes.set_expression(expr); + for (int i = 0; i < 180; i++) { // 3 seconds at 60fps + auto now = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(now - last_time).count(); + last_time = now; + eyes.update(dt); + std::this_thread::sleep_for(16ms); + } + } + + // Test look_at functionality using array iteration + logger.info("Testing look_at functionality"); + eyes.set_expression(espp::ExpressiveEyes::Expression::NEUTRAL); + + struct LookDirection { + const char *name; + float x; + float y; + }; + + const LookDirection look_directions[] = {{"left", -1.0f, 0.0f}, + {"right", 1.0f, 0.0f}, + {"up", 0.0f, -1.0f}, + {"down", 0.0f, 1.0f}, + {"center", 0.0f, 0.0f}}; + + for (const auto &dir : look_directions) { + logger.info("Looking {}", dir.name); + eyes.look_at(dir.x, dir.y); + for (int i = 0; i < 90; i++) { // 1.5 seconds at 60fps (faster) + auto now = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(now - last_time).count(); + last_time = now; + eyes.update(dt); + std::this_thread::sleep_for(16ms); + } + } + + // Back to normal with continuous updates + logger.info("Expression: Normal (continuous loop)"); + + // Animation loop + last_time = std::chrono::steady_clock::now(); + while (true) { + auto now = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(now - last_time).count(); + last_time = now; + + // Update and render eyes (calls draw callback) + eyes.update(dt); + + std::this_thread::sleep_for(16ms); // ~60 FPS + } + //! [expressive eyes example] + + logger.info("Expressive Eyes example complete!"); + + while (true) { + std::this_thread::sleep_for(1s); + } +} diff --git a/components/expressive_eyes/example/sdkconfig.defaults b/components/expressive_eyes/example/sdkconfig.defaults new file mode 100644 index 000000000..dc20163e6 --- /dev/null +++ b/components/expressive_eyes/example/sdkconfig.defaults @@ -0,0 +1,35 @@ +# ESP32-specific +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y + +# Common ESP-related +# +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# Flash size +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" + +# FreeRTOS +CONFIG_FREERTOS_HZ=1000 + +# PSRAM +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y + +# Default board selection +CONFIG_EXPRESSIVE_EYES_BOARD_ESP_BOX=y + +# Default settings +CONFIG_EXPRESSIVE_EYES_AUTO_DEMO=y +CONFIG_EXPRESSIVE_EYES_DEMO_INTERVAL_MS=3000 +CONFIG_EXPRESSIVE_EYES_ENABLE_PUPILS=y +CONFIG_EXPRESSIVE_EYES_PUPIL_SIZE=15 +CONFIG_EXPRESSIVE_EYES_AUTO_BLINK=y +CONFIG_EXPRESSIVE_EYES_BLINK_INTERVAL_MS=4000 +CONFIG_EXPRESSIVE_EYES_EYE_SPACING=100 +CONFIG_EXPRESSIVE_EYES_EYE_WIDTH=60 +CONFIG_EXPRESSIVE_EYES_EYE_HEIGHT=80 +CONFIG_EXPRESSIVE_EYES_COLOR=0xFFFF diff --git a/components/expressive_eyes/idf_component.yml b/components/expressive_eyes/idf_component.yml new file mode 100644 index 000000000..e9b3fe0cb --- /dev/null +++ b/components/expressive_eyes/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "Animated expressive eyes for displays" +url: "https://github.com/esp-cpp/espp/tree/main/components/expressive_eyes" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/expressive_eyes.html" +tags: + - cpp + - Animation + - Display +dependencies: + idf: + version: '>=5.0' + espp/logger: '>=1.0' + espp/task: '>=1.0' diff --git a/components/expressive_eyes/include/expressive_eyes.hpp b/components/expressive_eyes/include/expressive_eyes.hpp new file mode 100644 index 000000000..06d0e156f --- /dev/null +++ b/components/expressive_eyes/include/expressive_eyes.hpp @@ -0,0 +1,236 @@ +#pragma once + +#include +#include +#include +#include + +#include "base_component.hpp" + +namespace espp { +/** + * @brief Expressive Eyes Animation Component + * + * Renders animated expressive eyes using simple blob shapes. Eyes can blink, look around, + * change expression, and display various emotions. + * + * Features: + * - Smooth eye movement and blinking + * - Multiple expressions (happy, sad, angry, surprised, etc.) + * - Optional pupils with physics-based movement + * - Eyebrows and cheeks for enhanced expressions + * - Customizable colors and sizes + * - Frame-based animation system + * + * \section eyes_ex1 Expressive Eyes Example + * \snippet expressive_eyes_example.cpp expressive eyes example + */ +class ExpressiveEyes : public BaseComponent { +public: + /** + * @brief Pupil configuration + */ + struct Pupil { + bool enabled{true}; ///< Whether to draw pupils + float size{0.3f}; ///< Pupil size relative to eye (0.0-1.0) + uint16_t color{0x0000}; ///< Pupil color (black) + float x{0.0f}; ///< Pupil X position (-1.0 to 1.0) + float y{0.0f}; ///< Pupil Y position (-1.0 to 1.0) + }; + + /** + * @brief Eyebrow configuration + */ + struct Eyebrow { + bool enabled{false}; ///< Whether to draw eyebrows + float angle{0.0f}; ///< Eyebrow angle in radians + float height{0.0f}; ///< Vertical offset relative to eye (-1.0 to 1.0) + float thickness{0.1f}; ///< Eyebrow thickness relative to eye + float width{1.0f}; ///< Eyebrow width relative to eye + uint16_t color{0x0000}; ///< Eyebrow color (black) + }; + + /** + * @brief Cheek configuration (for shaping eye bottom) + */ + struct Cheek { + bool enabled{false}; ///< Whether to draw cheeks + float size{0.3f}; ///< Cheek size relative to eye + }; + + /** + * @brief Complete expression state + */ + struct ExpressionState { + float eye_width_scale{1.0f}; ///< Eye width multiplier + float eye_height_scale{1.0f}; ///< Eye height multiplier + float eye_rotation{0.0f}; ///< Eye rotation in radians + float top_curve{0.5f}; ///< Top eyelid curve (0=flat, 1=round) + float bottom_curve{0.5f}; ///< Bottom eyelid curve (0=flat, 1=round) + float eye_offset_y{0.0f}; ///< Vertical offset for eye position + float cheek_offset_y{0.0f}; ///< Vertical offset for cheek position + Pupil pupil; ///< Pupil configuration + Eyebrow eyebrow; ///< Eyebrow configuration + Cheek cheek; ///< Cheek configuration + }; + + /** + * @brief Single eye render data + */ + struct EyeState { + int x; ///< X coordinate of eye center + int y; ///< Y coordinate of eye center + int width; ///< Current eye width + int height; ///< Current eye height + ExpressionState expression; ///< Expression state for this eye + }; + + /** + * @brief Draw callback function + * @param left_eye Left eye render data + * @param right_eye Right eye render data + */ + typedef std::function draw_callback; + + /** + * @brief Eye expression presets + */ + enum class Expression { + NEUTRAL, ///< Normal open eyes + HAPPY, ///< Squinted happy eyes with raised eyebrows + SAD, ///< Droopy sad eyes with angled eyebrows + ANGRY, ///< Angled angry eyes with furrowed eyebrows + SURPRISED, ///< Wide open eyes with raised eyebrows + SLEEPY, ///< Half-closed eyes + WINK_LEFT, ///< Left eye closed + WINK_RIGHT, ///< Right eye closed + }; + + /** + * @brief Get string name for expression + * @param expr Expression enum + * @return String name of expression + */ + static constexpr const char *expression_name(Expression expr) { + switch (expr) { + case Expression::NEUTRAL: + return "Neutral"; + case Expression::HAPPY: + return "Happy"; + case Expression::SAD: + return "Sad"; + case Expression::ANGRY: + return "Angry"; + case Expression::SURPRISED: + return "Surprised"; + case Expression::SLEEPY: + return "Sleepy"; + case Expression::WINK_LEFT: + return "Wink Left"; + case Expression::WINK_RIGHT: + return "Wink Right"; + default: + return "Unknown"; + } + } + + /** + * @brief Configuration for expressive eyes + */ + struct Config { + int screen_width{320}; ///< Screen width in pixels + int screen_height{240}; ///< Screen height in pixels + int eye_spacing{100}; ///< Distance between eye centers + int eye_width{60}; ///< Base eye width + int eye_height{80}; ///< Base eye height + uint16_t eye_color{0xFFFF}; ///< Eye color (white) + float blink_duration{0.12f}; ///< Blink duration in seconds + float blink_interval{4.0f}; ///< Average time between blinks + bool enable_auto_blink{true}; ///< Automatic random blinking + bool enable_pupil_physics{true}; ///< Smooth pupil movement + draw_callback on_draw{nullptr}; ///< Drawing callback + Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity + }; + + /** + * @brief Construct expressive eyes + * @param config Configuration structure + */ + explicit ExpressiveEyes(const Config &config); + + /** + * @brief Update animation (call this every frame) + * @param dt Delta time since last update in seconds + */ + void update(float dt); + + /** + * @brief Set target look direction + * @param x Horizontal look direction (-1.0 to 1.0, 0=center) + * @param y Vertical look direction (-1.0 to 1.0, 0=center) + */ + void look_at(float x, float y); + + /** + * @brief Set expression + * @param expr Expression to display + */ + void set_expression(Expression expr); + + /** + * @brief Trigger a blink + */ + void blink(); + + /** + * @brief Get current expression + * @return Current expression + */ + Expression get_expression() const { return current_expression_; } + + /** + * @brief Get preset expression state + * @param expr Expression preset + * @return Expression state configuration + */ + static ExpressionState get_preset_expression(Expression expr); + +protected: + void init(const Config &config); + void update_blink(float dt); + void update_pupils(float dt); + void update_expression(float dt); + void draw_eyes(); + void blend_expression_states(ExpressionState &result, const ExpressionState &from, + const ExpressionState &to, float t); + + float lerp(float a, float b, float t) { return a + (b - a) * t; } + float clamp(float v, float min, float max) { return std::max(min, std::min(max, v)); } + + Config config_; + Expression current_expression_{Expression::NEUTRAL}; + Expression target_expression_{Expression::NEUTRAL}; + ExpressionState current_state_; + ExpressionState target_state_; + float expression_blend_{0.0f}; + + // Eye state + float blink_state_{0.0f}; // 0=open, 1=closed + float blink_timer_{0.0f}; + float next_blink_time_{4.0f}; + bool is_blinking_{false}; + + // Look direction + float target_look_x_{0.0f}; + float target_look_y_{0.0f}; + float current_look_x_{0.0f}; + float current_look_y_{0.0f}; + + // Pupil physics + float pupil_velocity_x_{0.0f}; + float pupil_velocity_y_{0.0f}; + + // Redraw throttling + float redraw_timer_{0.0f}; +}; +} // namespace espp diff --git a/components/expressive_eyes/src/expressive_eyes.cpp b/components/expressive_eyes/src/expressive_eyes.cpp new file mode 100644 index 000000000..94edc1ceb --- /dev/null +++ b/components/expressive_eyes/src/expressive_eyes.cpp @@ -0,0 +1,318 @@ +#include "expressive_eyes.hpp" +#include + +using namespace espp; + +ExpressiveEyes::ExpressiveEyes(const Config &config) + : BaseComponent("ExpressiveEyes", config.log_level) { + init(config); +} + +void ExpressiveEyes::init(const Config &config) { + config_ = config; + + // Initialize with neutral expression + current_state_ = get_preset_expression(Expression::NEUTRAL); + target_state_ = current_state_; + + // Randomize initial blink time + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_real_distribution dist(2.0f, config_.blink_interval * 1.5f); + next_blink_time_ = dist(gen); + + logger_.info("Initialized: {}x{}, eye_size={}x{}", config_.screen_width, config_.screen_height, + config_.eye_width, config_.eye_height); +} + +void ExpressiveEyes::update(float dt) { + update_blink(dt); + update_pupils(dt); + update_expression(dt); + + // Limit redraw rate to avoid watchdog + static constexpr float MIN_REDRAW_INTERVAL = 0.033f; // ~30 FPS max + redraw_timer_ += dt; + + if (redraw_timer_ >= MIN_REDRAW_INTERVAL) { + redraw_timer_ = 0.0f; + draw_eyes(); + } +} + +void ExpressiveEyes::update_blink(float dt) { + if (config_.enable_auto_blink) { + blink_timer_ += dt; + + if (!is_blinking_ && blink_timer_ >= next_blink_time_) { + blink(); + } + } + + if (is_blinking_) { + blink_state_ += dt / config_.blink_duration; + + if (blink_state_ >= 2.0f) { + // Blink complete + blink_state_ = 0.0f; + is_blinking_ = false; + blink_timer_ = 0.0f; + + // Random next blink time + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_real_distribution dist(config_.blink_interval * 0.5f, + config_.blink_interval * 1.5f); + next_blink_time_ = dist(gen); + } + } +} + +void ExpressiveEyes::update_pupils(float dt) { + if (!current_state_.pupil.enabled) + return; + + if (config_.enable_pupil_physics) { + // Spring physics for smooth pupil movement with critical damping to prevent overshoot + const float spring_strength = 40.0f; // Much stronger for faster response + const float damping = 15.0f; // Higher damping to prevent bounce + + float dx = target_look_x_ - current_look_x_; + float dy = target_look_y_ - current_look_y_; + + // Stop motion if very close to target + float distance = std::sqrt(dx * dx + dy * dy); + if (distance < 0.005f) { // Tighter threshold + current_look_x_ = target_look_x_; + current_look_y_ = target_look_y_; + pupil_velocity_x_ = 0.0f; + pupil_velocity_y_ = 0.0f; + } else { + pupil_velocity_x_ += dx * spring_strength * dt - pupil_velocity_x_ * damping * dt; + pupil_velocity_y_ += dy * spring_strength * dt - pupil_velocity_y_ * damping * dt; + + current_look_x_ += pupil_velocity_x_ * dt; + current_look_y_ += pupil_velocity_y_ * dt; + + // Clamp to valid range + current_look_x_ = clamp(current_look_x_, -1.0f, 1.0f); + current_look_y_ = clamp(current_look_y_, -1.0f, 1.0f); + } + } else { + // Direct movement + current_look_x_ = target_look_x_; + current_look_y_ = target_look_y_; + } + + // Update pupil position in state + current_state_.pupil.x = current_look_x_; + current_state_.pupil.y = current_look_y_; +} + +void ExpressiveEyes::update_expression(float dt) { + if (current_expression_ != target_expression_) { + // Start blend + current_expression_ = target_expression_; + target_state_ = get_preset_expression(target_expression_); + expression_blend_ = 0.0f; + } + + // Blend towards target + if (expression_blend_ < 1.0f) { + expression_blend_ += dt * 3.0f; // Blend over ~0.33 seconds (slower, smoother) + if (expression_blend_ > 1.0f) + expression_blend_ = 1.0f; + + ExpressionState from = get_preset_expression(current_expression_); + blend_expression_states(current_state_, from, target_state_, expression_blend_); + } +} + +void ExpressiveEyes::blend_expression_states(ExpressionState &result, const ExpressionState &from, + const ExpressionState &to, float t) { + result.eye_width_scale = lerp(from.eye_width_scale, to.eye_width_scale, t); + result.eye_height_scale = lerp(from.eye_height_scale, to.eye_height_scale, t); + result.eye_rotation = lerp(from.eye_rotation, to.eye_rotation, t); + result.top_curve = lerp(from.top_curve, to.top_curve, t); + result.bottom_curve = lerp(from.bottom_curve, to.bottom_curve, t); + result.eye_offset_y = lerp(from.eye_offset_y, to.eye_offset_y, t); + result.cheek_offset_y = lerp(from.cheek_offset_y, to.cheek_offset_y, t); + + // Blend eyebrow + result.eyebrow.enabled = from.eyebrow.enabled || to.eyebrow.enabled; + result.eyebrow.angle = lerp(from.eyebrow.angle, to.eyebrow.angle, t); + result.eyebrow.height = lerp(from.eyebrow.height, to.eyebrow.height, t); + result.eyebrow.thickness = lerp(from.eyebrow.thickness, to.eyebrow.thickness, t); + + // Blend cheek + result.cheek.enabled = from.cheek.enabled || to.cheek.enabled; + // Cheek enabled is binary, use the target state + result.cheek.enabled = t < 0.5f ? from.cheek.enabled : to.cheek.enabled; + + // Keep pupil settings from target + result.pupil = to.pupil; + result.pupil.x = current_look_x_; + result.pupil.y = current_look_y_; +} + +void ExpressiveEyes::draw_eyes() { + if (!config_.on_draw) + return; + + int center_x = config_.screen_width / 2; + int center_y = config_.screen_height / 2; + + int left_x = center_x - config_.eye_spacing / 2; + int right_x = center_x + config_.eye_spacing / 2; + + // Apply vertical offset from expression + int eye_y = center_y + static_cast(current_state_.eye_offset_y * config_.screen_height); + + // Calculate blink effect (0=open, 1=closed) + float blink_amount = 0.0f; + if (is_blinking_) { + if (blink_state_ <= 1.0f) { + // Closing (0 -> 1) + blink_amount = blink_state_; + } else { + // Opening (1 -> 0) + blink_amount = 2.0f - blink_state_; + } + } + + // Compute final eye dimensions + int eye_width = static_cast(config_.eye_width * current_state_.eye_width_scale); + int eye_height = static_cast(config_.eye_height * current_state_.eye_height_scale * + (1.0f - blink_amount)); + + // Create state for each eye + EyeState left_eye; + left_eye.x = left_x; + left_eye.y = eye_y; + left_eye.width = eye_width; + left_eye.height = eye_height; + left_eye.expression = current_state_; + + EyeState right_eye; + right_eye.x = right_x; + right_eye.y = eye_y; + right_eye.width = eye_width; + right_eye.height = eye_height; + right_eye.expression = current_state_; + right_eye.expression.eye_rotation = -current_state_.eye_rotation; // Mirror rotation + + // Draw both eyes in single call + config_.on_draw(left_eye, right_eye); +} + +void ExpressiveEyes::look_at(float x, float y) { + target_look_x_ = clamp(x, -1.0f, 1.0f); + target_look_y_ = clamp(y, -1.0f, 1.0f); +} + +void ExpressiveEyes::set_expression(Expression expr) { + target_expression_ = expr; + logger_.debug("Expression changed to: {}", static_cast(expr)); +} + +void ExpressiveEyes::blink() { + if (!is_blinking_) { + is_blinking_ = true; + blink_state_ = 0.0f; + logger_.debug("Blinking"); + } +} + +ExpressiveEyes::ExpressionState ExpressiveEyes::get_preset_expression(Expression expr) { + ExpressionState state; + + // Default pupil - keep size constant across all expressions + state.pupil.enabled = true; + state.pupil.size = 0.3f; // Constant size + state.pupil.color = 0x0000; + + switch (expr) { + case Expression::NEUTRAL: + state.eye_width_scale = 1.0f; + state.eye_height_scale = 1.0f; + state.eye_rotation = 0.0f; + state.top_curve = 0.5f; + state.bottom_curve = 0.5f; + break; + + case Expression::HAPPY: + state.eye_width_scale = 1.0f; // Same size as normal + state.eye_height_scale = 1.0f; // Same size as normal + state.eye_rotation = 0.0f; + state.top_curve = 0.5f; + state.bottom_curve = 0.5f; + state.eye_offset_y = -0.08f; // Move eyes up slightly on face + state.cheek_offset_y = 0.05f; // Move cheeks lower (positive is down) + // No eyebrows for happy (hidden by default) + state.cheek.enabled = true; // Shape bottom with cheeks + state.cheek.size = 0.6f; // Wider cheeks + break; + + case Expression::SAD: + state.eye_width_scale = 0.9f; + state.eye_height_scale = 0.85f; // Slightly drooped + state.eye_rotation = -0.25f; // Downward curve + state.top_curve = 0.3f; // Flatter top + state.bottom_curve = 0.7f; // Rounder bottom + state.eyebrow.enabled = true; + state.eyebrow.angle = -20.0f; // Angled UP toward center (sad brows) - in degrees + state.eyebrow.height = -0.35f; + state.eyebrow.thickness = 0.08f; + state.eyebrow.width = 1.0f; + state.eyebrow.color = 0x0000; + // Don't override pupil size + state.cheek.enabled = true; // Shape bottom with cheeks + state.cheek.size = 0.4f; + break; + + case Expression::ANGRY: + state.eye_width_scale = 0.85f; // Narrower + state.eye_height_scale = 0.75f; + state.eye_rotation = 0.4f; // Sharp inward angle + state.top_curve = 0.2f; // Angular top + state.bottom_curve = 0.3f; + state.eyebrow.enabled = true; + state.eyebrow.angle = 25.0f; // Angled DOWN toward center (angry furrowed brows) - in degrees + state.eyebrow.height = -0.2f; // Close to eyes + state.eyebrow.thickness = 0.12f; // Thicker + state.eyebrow.width = 0.9f; + state.eyebrow.color = 0x0000; + // Don't override pupil size + break; + + case Expression::SURPRISED: + state.eye_width_scale = 1.3f; // Wide open + state.eye_height_scale = 1.4f; + state.eye_rotation = 0.0f; + state.top_curve = 0.7f; + state.bottom_curve = 0.7f; + state.eyebrow.enabled = false; // No eyebrows for surprised + // Don't override pupil size + break; + + case Expression::SLEEPY: + state.eye_width_scale = 1.0f; + state.eye_height_scale = 0.25f; // Very closed + state.eye_rotation = 0.0f; + state.top_curve = 0.3f; + state.bottom_curve = 0.3f; + state.pupil.enabled = false; // Pupils hidden + break; + + case Expression::WINK_LEFT: + case Expression::WINK_RIGHT: + state.eye_width_scale = 1.0f; + state.eye_height_scale = 1.0f; + state.eye_rotation = 0.0f; + state.top_curve = 0.5f; + state.bottom_curve = 0.5f; + break; + } + + return state; +} From b17b8ac3113c73f6c5cfb0f61fe015323a5fabce Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 22 Jan 2026 09:53:58 -0600 Subject: [PATCH 02/21] fleshing out functionality to support multiple drawing styles and work with other hardware --- .../expressive_eyes/example/CMakeLists.txt | 2 +- .../example/main/Kconfig.projbuild | 26 +- .../example/main/README_DRAWERS.md | 102 ++++++++ .../example/main/expressive_eyes_example.cpp | 206 +++------------- .../example/main/eye_drawer.hpp | 24 ++ .../example/main/full_featured_drawer.hpp | 223 ++++++++++++++++++ .../example/main/monochrome_blue_drawer.hpp | 193 +++++++++++++++ .../expressive_eyes/src/expressive_eyes.cpp | 9 + 8 files changed, 604 insertions(+), 181 deletions(-) mode change 100644 => 100755 components/expressive_eyes/example/main/Kconfig.projbuild create mode 100755 components/expressive_eyes/example/main/README_DRAWERS.md create mode 100644 components/expressive_eyes/example/main/eye_drawer.hpp create mode 100644 components/expressive_eyes/example/main/full_featured_drawer.hpp create mode 100644 components/expressive_eyes/example/main/monochrome_blue_drawer.hpp mode change 100644 => 100755 components/expressive_eyes/src/expressive_eyes.cpp diff --git a/components/expressive_eyes/example/CMakeLists.txt b/components/expressive_eyes/example/CMakeLists.txt index 0c211e422..e27295bc2 100644 --- a/components/expressive_eyes/example/CMakeLists.txt +++ b/components/expressive_eyes/example/CMakeLists.txt @@ -12,7 +12,7 @@ set(EXTRA_COMPONENT_DIRS set( COMPONENTS - "main esptool_py expressive_eyes display task box box-3 esp-box esp-box-3" + "main esptool_py expressive_eyes display task esp-box matouch-rotary-display ws-s3-touch" CACHE STRING "List of components to include" ) diff --git a/components/expressive_eyes/example/main/Kconfig.projbuild b/components/expressive_eyes/example/main/Kconfig.projbuild old mode 100644 new mode 100755 index f2d15a458..8a6ea6087 --- a/components/expressive_eyes/example/main/Kconfig.projbuild +++ b/components/expressive_eyes/example/main/Kconfig.projbuild @@ -16,15 +16,29 @@ menu "Expressive Eyes Example Configuration" help Use MaTouch Rotary Display board. - config EXPRESSIVE_EYES_BOARD_WROVER_KIT - bool "ESP-WROVER-KIT" + config EXPRESSIVE_EYES_BOARD_WS_S3_TOUCH + bool "WS-S3-Touch BSP" help - Use ESP-WROVER-KIT development board. + Use WS-S3-Touch development board. - config EXPRESSIVE_EYES_BOARD_TAB5 - bool "M5Stack Tab5" + endchoice + + choice EXPRESSIVE_EYES_DRAWING_METHOD + prompt "Select Drawing Method" + default EXPRESSIVE_EYES_MONOCHROME_BLUE if EXPRESSIVE_EYES_BOARD_WS_S3_TOUCH + default EXPRESSIVE_EYES_FULL_FEATURED + help + Select the eye drawing implementation to use. + + config EXPRESSIVE_EYES_FULL_FEATURED + bool "Full Featured (White eyes with black pupils)" + help + Draw realistic eyes with white background, black pupils, eyebrows, and cheeks. + + config EXPRESSIVE_EYES_MONOCHROME_BLUE + bool "Monochrome Blue (Electric blue on black)" help - Use M5Stack Tab5 development board. + Draw simple monochrome eyes in electric blue on black background without pupils. endchoice diff --git a/components/expressive_eyes/example/main/README_DRAWERS.md b/components/expressive_eyes/example/main/README_DRAWERS.md new file mode 100755 index 000000000..d42aa18c9 --- /dev/null +++ b/components/expressive_eyes/example/main/README_DRAWERS.md @@ -0,0 +1,102 @@ +# Eye Drawer Implementations + +This directory contains different implementations for drawing expressive eyes. The drawing logic has been separated into modular components to demonstrate different visual styles and allow easy customization. + +## Architecture + +The drawer system uses a base interface (`EyeDrawer`) with concrete implementations: + +- **`eye_drawer.hpp`**: Base interface that all drawers must implement +- **`full_featured_drawer.hpp`**: Traditional realistic eyes with white background and black pupils +- **`monochrome_blue_drawer.hpp`**: Electric blue eyes on black background (no pupils) + +## Available Drawers + +### Full Featured Drawer + +Draws realistic-looking eyes with: +- White eye background +- Black pupils with position tracking +- Eyebrows with rotation support +- Cheeks for emotional expressions +- Uses background color from LVGL theme + +**Best for**: Traditional robot/character displays, realistic eye expressions + +### Monochrome Blue Drawer + +Draws minimalist electric blue eyes with: +- Electric blue (#00BFFF) eye shapes +- Black background +- No pupils (solid colored eyes) +- Black eyebrows and cheeks (cut out from eyes as negative space) +- Clean, modern aesthetic + +**Best for**: Sci-fi/futuristic interfaces, minimal power displays, WS-S3-Touch boards + +## Configuration + +The drawing method is selected via menuconfig: +``` +Expressive Eyes Example Configuration > Select Drawing Method +``` + +Options: +- **Full Featured** (default for most boards) +- **Monochrome Blue** (default for WS-S3-Touch) + +## Adding New Drawers + +To create a new drawer implementation: + +1. Create a new header file (e.g., `my_custom_drawer.hpp`) +2. Inherit from `eye_drawer::EyeDrawer` +3. Implement required methods: + - `get_draw_callback()`: Return the drawing function + - `cleanup()`: Clean up resources if needed +4. Add config option to `Kconfig.projbuild` +5. Add conditional compilation to `expressive_eyes_example.cpp` + +### Example Template + +```cpp +#pragma once +#include "eye_drawer.hpp" + +namespace eye_drawer { + +class MyCustomDrawer : public EyeDrawer { +public: + struct Config { + int screen_width; + int screen_height; + lv_obj_t *canvas; + lv_color_t *canvas_buffer; + std::recursive_mutex &lvgl_mutex; + }; + + explicit MyCustomDrawer(const Config &config) { /* ... */ } + + std::function + get_draw_callback() override { + return [this](const espp::ExpressiveEyes::EyeState &left, + const espp::ExpressiveEyes::EyeState &right) { + // Your drawing code here + }; + } + + void cleanup() override { /* ... */ } +}; + +} // namespace eye_drawer +``` + +## Drawing Tips + +- Use `lv_canvas_init_layer()` and `lv_canvas_finish_layer()` for each drawing operation +- Access eye state through `EyeState` structure (position, size, expression) +- Lock `lvgl_mutex` before any LVGL operations +- Use `eye_state.height / original_eye_height` to detect blink state +- Mirror eyebrow angles for left vs right eyes +- Use `screen_width` and `screen_height` for relative positioning diff --git a/components/expressive_eyes/example/main/expressive_eyes_example.cpp b/components/expressive_eyes/example/main/expressive_eyes_example.cpp index 5c0df4ee7..4fdabefb6 100644 --- a/components/expressive_eyes/example/main/expressive_eyes_example.cpp +++ b/components/expressive_eyes/example/main/expressive_eyes_example.cpp @@ -7,6 +7,10 @@ #include "logger.hpp" #include "task.hpp" +// Include drawer implementations +#include "full_featured_drawer.hpp" +#include "monochrome_blue_drawer.hpp" + // Board-specific includes based on menuconfig selection #if CONFIG_EXPRESSIVE_EYES_BOARD_ESP_BOX #include "esp-box.hpp" @@ -14,12 +18,9 @@ using Board = espp::EspBox; #elif CONFIG_EXPRESSIVE_EYES_BOARD_MATOUCH_ROTARY #include "matouch-rotary-display.hpp" using Board = espp::MatouchRotaryDisplay; -#elif CONFIG_EXPRESSIVE_EYES_BOARD_WROVER_KIT -#include "wrover-kit.hpp" -using Board = espp::WroverKit; -#elif CONFIG_EXPRESSIVE_EYES_BOARD_TAB5 -#include "m5stack-tab5.hpp" -using Board = espp::M5StackTab5; +#elif CONFIG_EXPRESSIVE_EYES_BOARD_WS_S3_TOUCH +#include "ws-s3-touch.hpp" +using Board = espp::WsS3Touch; #else #error "No board selected in menuconfig!" #endif @@ -62,9 +63,6 @@ extern "C" void app_main(void) { // Disable scrollbars on screen lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); - // Get background color - lv_color_t bg_color = lv_obj_get_style_bg_color(lv_screen_active(), LV_PART_MAIN); - // Create main canvas for drawing everything lv_obj_t *canvas = lv_canvas_create(lv_screen_active()); @@ -80,171 +78,31 @@ extern "C" void app_main(void) { lv_canvas_set_buffer(canvas, canvas_buffer, screen_width, screen_height, LV_COLOR_FORMAT_RGB565); lv_obj_center(canvas); - // Store original eye dimensions for calculations - int original_eye_height = screen_height * 0.55f; - int eye_base_width = screen_width * 0.35f; - int pupil_size = static_cast(std::min(eye_base_width, original_eye_height) * 0.3f); - - // Drawing callback for eyes - single call for both eyes using canvas - auto draw_eyes = [&](const espp::ExpressiveEyes::EyeState &left, - const espp::ExpressiveEyes::EyeState &right) { - std::lock_guard lock(lvgl_mutex); - - // Clear canvas with background color - lv_canvas_fill_bg(canvas, bg_color, LV_OPA_COVER); - - // Helper to draw filled ellipse (for eyes) using layer API - auto draw_ellipse = [&](int cx, int cy, int width, int height, lv_color_t color) { - lv_layer_t layer; - lv_canvas_init_layer(canvas, &layer); - - lv_draw_rect_dsc_t rect_dsc; - lv_draw_rect_dsc_init(&rect_dsc); - rect_dsc.bg_color = color; - rect_dsc.bg_opa = LV_OPA_COVER; - rect_dsc.radius = LV_RADIUS_CIRCLE; - rect_dsc.border_width = 0; - - lv_area_t area; - area.x1 = cx - width / 2; - area.y1 = cy - height / 2; - area.x2 = cx + width / 2; - area.y2 = cy + height / 2; - - lv_draw_rect(&layer, &rect_dsc, &area); - lv_canvas_finish_layer(canvas, &layer); - }; - - // Lambda to draw single eye - auto draw_single_eye = [&](const espp::ExpressiveEyes::EyeState &eye_state, bool is_left) { - // Draw eye white - draw_ellipse(eye_state.x, eye_state.y, eye_state.width, eye_state.height, lv_color_white()); - - // Draw pupil (use original height so blink doesn't move pupil) - // Only draw if eye is open enough (not blinking) - float openness = eye_state.height / static_cast(original_eye_height); - if (eye_state.expression.pupil.enabled && openness > 0.3f) { - int px_offset = static_cast(eye_state.expression.pupil.x * eye_state.width * 0.3f); - int py_offset = static_cast(eye_state.expression.pupil.y * original_eye_height * 0.3f); - int pupil_x = eye_state.x + px_offset; - int pupil_y = eye_state.y + py_offset; - - lv_layer_t layer; - lv_canvas_init_layer(canvas, &layer); - - lv_draw_rect_dsc_t rect_dsc; - lv_draw_rect_dsc_init(&rect_dsc); - rect_dsc.bg_color = lv_color_black(); - rect_dsc.bg_opa = LV_OPA_COVER; - rect_dsc.radius = LV_RADIUS_CIRCLE; - rect_dsc.border_width = 0; - - lv_area_t area; - int r = pupil_size / 2; - area.x1 = pupil_x - r; - area.y1 = pupil_y - r; - area.x2 = pupil_x + r; - area.y2 = pupil_y + r; - - lv_draw_rect(&layer, &rect_dsc, &area); - lv_canvas_finish_layer(canvas, &layer); - } - - // Draw eyebrow as rotated rectangle (using triangles) - if (eye_state.expression.eyebrow.enabled) { - int brow_width = - static_cast(eye_base_width * eye_state.expression.eyebrow.width * 1.5f); - int brow_height = - static_cast(eye_base_width * eye_state.expression.eyebrow.thickness * 4.0f); - int brow_y = eye_state.y - static_cast(original_eye_height * 0.4f); - - // For left eye, positive angle tilts left side down (clockwise rotation) - // For right eye, positive angle tilts right side down (counter-clockwise rotation) - float angle_rad = eye_state.expression.eyebrow.angle * M_PI / 180.0f; - if (!is_left) - angle_rad = -angle_rad; // Mirror for right eye - - // Calculate the 4 corners of a rotated rectangle centered at (eye_state.x, brow_y) - float half_w = brow_width / 2.0f; - float half_h = brow_height / 2.0f; - float cos_a = std::cos(angle_rad); - float sin_a = std::sin(angle_rad); - - // Four corners relative to center, then rotated - int x1 = eye_state.x + static_cast(-half_w * cos_a + half_h * sin_a); - int y1 = brow_y + static_cast(-half_w * sin_a - half_h * cos_a); - - int x2 = eye_state.x + static_cast(half_w * cos_a + half_h * sin_a); - int y2 = brow_y + static_cast(half_w * sin_a - half_h * cos_a); - - int x3 = eye_state.x + static_cast(half_w * cos_a - half_h * sin_a); - int y3 = brow_y + static_cast(half_w * sin_a + half_h * cos_a); - - int x4 = eye_state.x + static_cast(-half_w * cos_a - half_h * sin_a); - int y4 = brow_y + static_cast(-half_w * sin_a + half_h * cos_a); - - lv_layer_t layer; - lv_canvas_init_layer(canvas, &layer); - - lv_draw_triangle_dsc_t tri_dsc; - lv_draw_triangle_dsc_init(&tri_dsc); - tri_dsc.color = bg_color; - tri_dsc.opa = LV_OPA_COVER; - - // First triangle: top-left, top-right, bottom-right - tri_dsc.p[0].x = x1; - tri_dsc.p[0].y = y1; - tri_dsc.p[1].x = x2; - tri_dsc.p[1].y = y2; - tri_dsc.p[2].x = x3; - tri_dsc.p[2].y = y3; - lv_draw_triangle(&layer, &tri_dsc); - - // Second triangle: top-left, bottom-right, bottom-left - tri_dsc.p[0].x = x1; - tri_dsc.p[0].y = y1; - tri_dsc.p[1].x = x3; - tri_dsc.p[1].y = y3; - tri_dsc.p[2].x = x4; - tri_dsc.p[2].y = y4; - lv_draw_triangle(&layer, &tri_dsc); - - lv_canvas_finish_layer(canvas, &layer); - } - - // Draw cheek - if (eye_state.expression.cheek.enabled) { - int cheek_width = static_cast(eye_base_width * eye_state.expression.cheek.size * 2.5f); - int cheek_height = - static_cast(eye_base_width * eye_state.expression.cheek.size * 1.5f); - int cheek_y = eye_state.y + static_cast(original_eye_height * 0.4f) + - static_cast(eye_state.expression.cheek_offset_y * screen_height); - - lv_layer_t layer; - lv_canvas_init_layer(canvas, &layer); - - lv_draw_rect_dsc_t rect_dsc; - lv_draw_rect_dsc_init(&rect_dsc); - rect_dsc.bg_color = bg_color; - rect_dsc.bg_opa = LV_OPA_COVER; - rect_dsc.radius = LV_RADIUS_CIRCLE; - rect_dsc.border_width = 0; - - lv_area_t area; - area.x1 = eye_state.x - cheek_width / 2; - area.y1 = cheek_y - cheek_height / 2; - area.x2 = eye_state.x + cheek_width / 2; - area.y2 = cheek_y + cheek_height / 2; - - lv_draw_rect(&layer, &rect_dsc, &area); - lv_canvas_finish_layer(canvas, &layer); - } - }; - - // Draw both eyes - draw_single_eye(left, true); - draw_single_eye(right, false); - }; + // Create the drawer based on menuconfig selection + std::unique_ptr drawer; + +#if CONFIG_EXPRESSIVE_EYES_FULL_FEATURED + logger.info("Using Full Featured drawer"); + drawer = std::make_unique( + eye_drawer::FullFeaturedDrawer::Config{.screen_width = screen_width, + .screen_height = screen_height, + .canvas = canvas, + .canvas_buffer = canvas_buffer, + .lvgl_mutex = lvgl_mutex}); +#elif CONFIG_EXPRESSIVE_EYES_MONOCHROME_BLUE + logger.info("Using Monochrome Blue drawer"); + drawer = std::make_unique( + eye_drawer::MonochromeBlueDrawer::Config{.screen_width = screen_width, + .screen_height = screen_height, + .canvas = canvas, + .canvas_buffer = canvas_buffer, + .lvgl_mutex = lvgl_mutex}); +#else +#error "No drawing method selected in menuconfig!" +#endif + + // Get the draw callback from the drawer + auto draw_eyes = drawer->get_draw_callback(); // Configure expressive eyes with adaptive sizing - make eyes larger int large_eye_width = screen_width * 0.35f; // 35% of screen width each diff --git a/components/expressive_eyes/example/main/eye_drawer.hpp b/components/expressive_eyes/example/main/eye_drawer.hpp new file mode 100644 index 000000000..bd2d57531 --- /dev/null +++ b/components/expressive_eyes/example/main/eye_drawer.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "expressive_eyes.hpp" +#include + +#include + +namespace eye_drawer { + +/// \brief Base interface for eye drawing implementations +struct EyeDrawer { + virtual ~EyeDrawer() = default; + + /// \brief Get the draw callback function + /// \return The callback function to be used with ExpressiveEyes + virtual std::function + get_draw_callback() = 0; + + /// \brief Clean up resources + virtual void cleanup() = 0; +}; + +} // namespace eye_drawer diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp new file mode 100644 index 000000000..a6b1a1ed2 --- /dev/null +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -0,0 +1,223 @@ +#pragma once + +#include "eye_drawer.hpp" +#include +#include + +#include + +namespace eye_drawer { + +/// \brief Full-featured eye drawer with white eyes, black pupils, and eyebrows +/// \details Draws realistic eyes with: +/// - White eye background +/// - Black pupils with position control +/// - Eyebrows with rotation +/// - Cheeks +class FullFeaturedDrawer : public EyeDrawer { +public: + struct Config { + int screen_width; + int screen_height; + lv_obj_t *canvas; + lv_color_t *canvas_buffer; + std::recursive_mutex &lvgl_mutex; + }; + + explicit FullFeaturedDrawer(const Config &config) + : screen_width_(config.screen_width) + , screen_height_(config.screen_height) + , canvas_(config.canvas) + , canvas_buffer_(config.canvas_buffer) + , lvgl_mutex_(config.lvgl_mutex) { + // Calculate eye dimensions + original_eye_height_ = screen_height_ * 0.55f; + eye_base_width_ = screen_width_ * 0.35f; + pupil_size_ = static_cast(std::min(eye_base_width_, original_eye_height_) * 0.3f); + } + + ~FullFeaturedDrawer() override { cleanup(); } + + std::function + get_draw_callback() override { + return [this](const espp::ExpressiveEyes::EyeState &left, + const espp::ExpressiveEyes::EyeState &right) { + std::lock_guard lock(lvgl_mutex_); + + // Get background color + lv_color_t bg_color = lv_obj_get_style_bg_color(lv_screen_active(), LV_PART_MAIN); + + // Clear canvas with background color + lv_canvas_fill_bg(canvas_, bg_color, LV_OPA_COVER); + + // Draw both eyes + draw_single_eye(left, true, bg_color); + draw_single_eye(right, false, bg_color); + }; + } + + void cleanup() override { + // Nothing to clean up in this implementation + } + +private: + // Helper to draw filled ellipse (for eyes) using layer API + void draw_ellipse(int cx, int cy, int width, int height, lv_color_t color) { + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = color; + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + area.x1 = cx - width / 2; + area.y1 = cy - height / 2; + area.x2 = cx + width / 2; + area.y2 = cy + height / 2; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas_, &layer); + } + + void draw_single_eye(const espp::ExpressiveEyes::EyeState &eye_state, bool is_left, + lv_color_t bg_color) { + // Draw eye white + draw_ellipse(eye_state.x, eye_state.y, eye_state.width, eye_state.height, lv_color_white()); + + // Draw pupil (use original height so blink doesn't move pupil) + // Only draw if eye is open enough (not blinking) + float openness = eye_state.height / static_cast(original_eye_height_); + if (eye_state.expression.pupil.enabled && openness > 0.3f) { + int px_offset = static_cast(eye_state.expression.pupil.x * eye_state.width * 0.3f); + int py_offset = static_cast(eye_state.expression.pupil.y * original_eye_height_ * 0.3f); + int pupil_x = eye_state.x + px_offset; + int pupil_y = eye_state.y + py_offset; + + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = lv_color_black(); + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + int r = pupil_size_ / 2; + area.x1 = pupil_x - r; + area.y1 = pupil_y - r; + area.x2 = pupil_x + r; + area.y2 = pupil_y + r; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas_, &layer); + } + + // Draw eyebrow as rotated rectangle (using triangles) + if (eye_state.expression.eyebrow.enabled) { + int brow_width = + static_cast(eye_base_width_ * eye_state.expression.eyebrow.width * 1.5f); + int brow_height = + static_cast(eye_base_width_ * eye_state.expression.eyebrow.thickness * 4.0f); + int brow_y = eye_state.y - static_cast(original_eye_height_ * 0.4f); + + // For left eye, positive angle tilts left side down (clockwise rotation) + // For right eye, positive angle tilts right side down (counter-clockwise rotation) + float angle_rad = eye_state.expression.eyebrow.angle * M_PI / 180.0f; + if (!is_left) + angle_rad = -angle_rad; // Mirror for right eye + + // Calculate the 4 corners of a rotated rectangle centered at (eye_state.x, brow_y) + float half_w = brow_width / 2.0f; + float half_h = brow_height / 2.0f; + float cos_a = std::cos(angle_rad); + float sin_a = std::sin(angle_rad); + + // Four corners relative to center, then rotated + int x1 = eye_state.x + static_cast(-half_w * cos_a + half_h * sin_a); + int y1 = brow_y + static_cast(-half_w * sin_a - half_h * cos_a); + + int x2 = eye_state.x + static_cast(half_w * cos_a + half_h * sin_a); + int y2 = brow_y + static_cast(half_w * sin_a - half_h * cos_a); + + int x3 = eye_state.x + static_cast(half_w * cos_a - half_h * sin_a); + int y3 = brow_y + static_cast(half_w * sin_a + half_h * cos_a); + + int x4 = eye_state.x + static_cast(-half_w * cos_a - half_h * sin_a); + int y4 = brow_y + static_cast(-half_w * sin_a + half_h * cos_a); + + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_triangle_dsc_t tri_dsc; + lv_draw_triangle_dsc_init(&tri_dsc); + tri_dsc.color = bg_color; + tri_dsc.opa = LV_OPA_COVER; + + // First triangle: top-left, top-right, bottom-right + tri_dsc.p[0].x = x1; + tri_dsc.p[0].y = y1; + tri_dsc.p[1].x = x2; + tri_dsc.p[1].y = y2; + tri_dsc.p[2].x = x3; + tri_dsc.p[2].y = y3; + lv_draw_triangle(&layer, &tri_dsc); + + // Second triangle: top-left, bottom-right, bottom-left + tri_dsc.p[0].x = x1; + tri_dsc.p[0].y = y1; + tri_dsc.p[1].x = x3; + tri_dsc.p[1].y = y3; + tri_dsc.p[2].x = x4; + tri_dsc.p[2].y = y4; + lv_draw_triangle(&layer, &tri_dsc); + + lv_canvas_finish_layer(canvas_, &layer); + } + + // Draw cheek + if (eye_state.expression.cheek.enabled) { + int cheek_width = static_cast(eye_base_width_ * eye_state.expression.cheek.size * 2.5f); + int cheek_height = static_cast(eye_base_width_ * eye_state.expression.cheek.size * 1.5f); + int cheek_y = eye_state.y + static_cast(original_eye_height_ * 0.4f) + + static_cast(eye_state.expression.cheek_offset_y * screen_height_); + + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = bg_color; + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + area.x1 = eye_state.x - cheek_width / 2; + area.y1 = cheek_y - cheek_height / 2; + area.x2 = eye_state.x + cheek_width / 2; + area.y2 = cheek_y + cheek_height / 2; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas_, &layer); + } + } + + int screen_width_; + int screen_height_; + lv_obj_t *canvas_; + lv_color_t *canvas_buffer_; + std::recursive_mutex &lvgl_mutex_; + + int original_eye_height_; + int eye_base_width_; + int pupil_size_; +}; + +} // namespace eye_drawer diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp new file mode 100644 index 000000000..81b18c554 --- /dev/null +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include "eye_drawer.hpp" +#include +#include + +#include + +namespace eye_drawer { + +/// \brief Monochrome eye drawer with electric blue eyes on black background +/// \details Draws simple eyes with: +/// - Electric blue eye shapes (no pupils) +/// - Black background +/// - Eyebrows in electric blue +/// - Cheeks in electric blue +class MonochromeBlueDrawer : public EyeDrawer { +public: + struct Config { + int screen_width; + int screen_height; + lv_obj_t *canvas; + lv_color_t *canvas_buffer; + std::recursive_mutex &lvgl_mutex; + }; + + explicit MonochromeBlueDrawer(const Config &config) + : screen_width_(config.screen_width) + , screen_height_(config.screen_height) + , canvas_(config.canvas) + , canvas_buffer_(config.canvas_buffer) + , lvgl_mutex_(config.lvgl_mutex) { + // Calculate eye dimensions + original_eye_height_ = screen_height_ * 0.55f; + eye_base_width_ = screen_width_ * 0.35f; + + // Electric blue color - RGB565 format + // R: 0x00 (0), G: 0xBF (191 -> 23 in 6-bit), B: 0xFF (255 -> 31 in 5-bit) + // RGB565: RRRRRGGGGGGBBBBB = 00000101111111111 = 0x05FF + electric_blue_ = lv_color_hex(0x00BFFF); + } + + ~MonochromeBlueDrawer() override { cleanup(); } + + std::function + get_draw_callback() override { + return [this](const espp::ExpressiveEyes::EyeState &left, + const espp::ExpressiveEyes::EyeState &right) { + std::lock_guard lock(lvgl_mutex_); + + // Clear canvas with black background + lv_canvas_fill_bg(canvas_, lv_color_black(), LV_OPA_COVER); + + // Draw both eyes + draw_single_eye(left, true); + draw_single_eye(right, false); + }; + } + + void cleanup() override { + // Nothing to clean up in this implementation + } + +private: + // Helper to draw filled ellipse using layer API + void draw_ellipse(int cx, int cy, int width, int height, lv_color_t color) { + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = color; + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + area.x1 = cx - width / 2; + area.y1 = cy - height / 2; + area.x2 = cx + width / 2; + area.y2 = cy + height / 2; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas_, &layer); + } + + void draw_single_eye(const espp::ExpressiveEyes::EyeState &eye_state, bool is_left) { + // Draw eye in electric blue (no pupil) + draw_ellipse(eye_state.x, eye_state.y, eye_state.width, eye_state.height, electric_blue_); + + // Draw eyebrow as rotated rectangle (using triangles) + if (eye_state.expression.eyebrow.enabled) { + int brow_width = + static_cast(eye_base_width_ * eye_state.expression.eyebrow.width * 1.5f); + int brow_height = + static_cast(eye_base_width_ * eye_state.expression.eyebrow.thickness * 4.0f); + int brow_y = eye_state.y - static_cast(original_eye_height_ * 0.4f); + + // For left eye, positive angle tilts left side down (clockwise rotation) + // For right eye, positive angle tilts right side down (counter-clockwise rotation) + float angle_rad = eye_state.expression.eyebrow.angle * M_PI / 180.0f; + if (!is_left) + angle_rad = -angle_rad; // Mirror for right eye + + // Calculate the 4 corners of a rotated rectangle centered at (eye_state.x, brow_y) + float half_w = brow_width / 2.0f; + float half_h = brow_height / 2.0f; + float cos_a = std::cos(angle_rad); + float sin_a = std::sin(angle_rad); + + // Four corners relative to center, then rotated + int x1 = eye_state.x + static_cast(-half_w * cos_a + half_h * sin_a); + int y1 = brow_y + static_cast(-half_w * sin_a - half_h * cos_a); + + int x2 = eye_state.x + static_cast(half_w * cos_a + half_h * sin_a); + int y2 = brow_y + static_cast(half_w * sin_a - half_h * cos_a); + + int x3 = eye_state.x + static_cast(half_w * cos_a - half_h * sin_a); + int y3 = brow_y + static_cast(half_w * sin_a + half_h * cos_a); + + int x4 = eye_state.x + static_cast(-half_w * cos_a - half_h * sin_a); + int y4 = brow_y + static_cast(-half_w * sin_a + half_h * cos_a); + + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_triangle_dsc_t tri_dsc; + lv_draw_triangle_dsc_init(&tri_dsc); + tri_dsc.color = lv_color_black(); + tri_dsc.opa = LV_OPA_COVER; + + // First triangle: top-left, top-right, bottom-right + tri_dsc.p[0].x = x1; + tri_dsc.p[0].y = y1; + tri_dsc.p[1].x = x2; + tri_dsc.p[1].y = y2; + tri_dsc.p[2].x = x3; + tri_dsc.p[2].y = y3; + lv_draw_triangle(&layer, &tri_dsc); + + // Second triangle: top-left, bottom-right, bottom-left + tri_dsc.p[0].x = x1; + tri_dsc.p[0].y = y1; + tri_dsc.p[1].x = x3; + tri_dsc.p[1].y = y3; + tri_dsc.p[2].x = x4; + tri_dsc.p[2].y = y4; + lv_draw_triangle(&layer, &tri_dsc); + + lv_canvas_finish_layer(canvas_, &layer); + } + + // Draw cheek in electric blue + if (eye_state.expression.cheek.enabled) { + int cheek_width = static_cast(eye_base_width_ * eye_state.expression.cheek.size * 2.5f); + int cheek_height = static_cast(eye_base_width_ * eye_state.expression.cheek.size * 1.5f); + int cheek_y = eye_state.y + static_cast(original_eye_height_ * 0.4f) + + static_cast(eye_state.expression.cheek_offset_y * screen_height_); + + lv_layer_t layer; + lv_canvas_init_layer(canvas_, &layer); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = lv_color_black(); + rect_dsc.bg_opa = LV_OPA_COVER; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.border_width = 0; + + lv_area_t area; + area.x1 = eye_state.x - cheek_width / 2; + area.y1 = cheek_y - cheek_height / 2; + area.x2 = eye_state.x + cheek_width / 2; + area.y2 = cheek_y + cheek_height / 2; + + lv_draw_rect(&layer, &rect_dsc, &area); + lv_canvas_finish_layer(canvas_, &layer); + } + } + + int screen_width_; + int screen_height_; + lv_obj_t *canvas_; + lv_color_t *canvas_buffer_; + std::recursive_mutex &lvgl_mutex_; + + int original_eye_height_; + int eye_base_width_; + lv_color_t electric_blue_; +}; + +} // namespace eye_drawer diff --git a/components/expressive_eyes/src/expressive_eyes.cpp b/components/expressive_eyes/src/expressive_eyes.cpp old mode 100644 new mode 100755 index 94edc1ceb..ab52aeee4 --- a/components/expressive_eyes/src/expressive_eyes.cpp +++ b/components/expressive_eyes/src/expressive_eyes.cpp @@ -168,6 +168,15 @@ void ExpressiveEyes::draw_eyes() { // Apply vertical offset from expression int eye_y = center_y + static_cast(current_state_.eye_offset_y * config_.screen_height); + // Apply eye position offset based on look direction + // Scale the offset to be smaller than pupil movement (about 15% of eye width/height) + int eye_offset_x = static_cast(current_look_x_ * config_.eye_width * 0.15f); + int eye_offset_y = static_cast(current_look_y_ * config_.eye_height * 0.15f); + + left_x += eye_offset_x; + right_x += eye_offset_x; + eye_y += eye_offset_y; + // Calculate blink effect (0=open, 1=closed) float blink_amount = 0.0f; if (is_blinking_) { From 9e00c36ace0da96bad4ba3b7ebef4e10603753be Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Jan 2026 08:28:03 -0600 Subject: [PATCH 03/21] replace eyebrow triangles with eyebrow line --- .../example/main/full_featured_drawer.hpp | 55 ++++++------------- .../example/main/monochrome_blue_drawer.hpp | 55 ++++++------------- 2 files changed, 36 insertions(+), 74 deletions(-) mode change 100644 => 100755 components/expressive_eyes/example/main/full_featured_drawer.hpp mode change 100644 => 100755 components/expressive_eyes/example/main/monochrome_blue_drawer.hpp diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp old mode 100644 new mode 100755 index a6b1a1ed2..f23b72a7e --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -119,7 +119,7 @@ class FullFeaturedDrawer : public EyeDrawer { lv_canvas_finish_layer(canvas_, &layer); } - // Draw eyebrow as rotated rectangle (using triangles) + // Draw eyebrow as rotated line if (eye_state.expression.eyebrow.enabled) { int brow_width = static_cast(eye_base_width_ * eye_state.expression.eyebrow.width * 1.5f); @@ -133,50 +133,31 @@ class FullFeaturedDrawer : public EyeDrawer { if (!is_left) angle_rad = -angle_rad; // Mirror for right eye - // Calculate the 4 corners of a rotated rectangle centered at (eye_state.x, brow_y) + // Calculate the two endpoints of the line float half_w = brow_width / 2.0f; - float half_h = brow_height / 2.0f; float cos_a = std::cos(angle_rad); float sin_a = std::sin(angle_rad); - // Four corners relative to center, then rotated - int x1 = eye_state.x + static_cast(-half_w * cos_a + half_h * sin_a); - int y1 = brow_y + static_cast(-half_w * sin_a - half_h * cos_a); - - int x2 = eye_state.x + static_cast(half_w * cos_a + half_h * sin_a); - int y2 = brow_y + static_cast(half_w * sin_a - half_h * cos_a); - - int x3 = eye_state.x + static_cast(half_w * cos_a - half_h * sin_a); - int y3 = brow_y + static_cast(half_w * sin_a + half_h * cos_a); - - int x4 = eye_state.x + static_cast(-half_w * cos_a - half_h * sin_a); - int y4 = brow_y + static_cast(-half_w * sin_a + half_h * cos_a); + lv_point_precise_t p1, p2; + p1.x = eye_state.x - half_w * cos_a; + p1.y = brow_y - half_w * sin_a; + p2.x = eye_state.x + half_w * cos_a; + p2.y = brow_y + half_w * sin_a; lv_layer_t layer; lv_canvas_init_layer(canvas_, &layer); - lv_draw_triangle_dsc_t tri_dsc; - lv_draw_triangle_dsc_init(&tri_dsc); - tri_dsc.color = bg_color; - tri_dsc.opa = LV_OPA_COVER; - - // First triangle: top-left, top-right, bottom-right - tri_dsc.p[0].x = x1; - tri_dsc.p[0].y = y1; - tri_dsc.p[1].x = x2; - tri_dsc.p[1].y = y2; - tri_dsc.p[2].x = x3; - tri_dsc.p[2].y = y3; - lv_draw_triangle(&layer, &tri_dsc); - - // Second triangle: top-left, bottom-right, bottom-left - tri_dsc.p[0].x = x1; - tri_dsc.p[0].y = y1; - tri_dsc.p[1].x = x3; - tri_dsc.p[1].y = y3; - tri_dsc.p[2].x = x4; - tri_dsc.p[2].y = y4; - lv_draw_triangle(&layer, &tri_dsc); + lv_draw_line_dsc_t line_dsc; + lv_draw_line_dsc_init(&line_dsc); + line_dsc.color = bg_color; + line_dsc.width = brow_height; + line_dsc.opa = LV_OPA_COVER; + line_dsc.round_start = 1; + line_dsc.round_end = 1; + line_dsc.p1 = p1; + line_dsc.p2 = p2; + + lv_draw_line(&layer, &line_dsc); lv_canvas_finish_layer(canvas_, &layer); } diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp old mode 100644 new mode 100755 index 81b18c554..e85dd6e60 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -89,7 +89,7 @@ class MonochromeBlueDrawer : public EyeDrawer { // Draw eye in electric blue (no pupil) draw_ellipse(eye_state.x, eye_state.y, eye_state.width, eye_state.height, electric_blue_); - // Draw eyebrow as rotated rectangle (using triangles) + // Draw eyebrow as rotated line if (eye_state.expression.eyebrow.enabled) { int brow_width = static_cast(eye_base_width_ * eye_state.expression.eyebrow.width * 1.5f); @@ -103,50 +103,31 @@ class MonochromeBlueDrawer : public EyeDrawer { if (!is_left) angle_rad = -angle_rad; // Mirror for right eye - // Calculate the 4 corners of a rotated rectangle centered at (eye_state.x, brow_y) + // Calculate the two endpoints of the line float half_w = brow_width / 2.0f; - float half_h = brow_height / 2.0f; float cos_a = std::cos(angle_rad); float sin_a = std::sin(angle_rad); - // Four corners relative to center, then rotated - int x1 = eye_state.x + static_cast(-half_w * cos_a + half_h * sin_a); - int y1 = brow_y + static_cast(-half_w * sin_a - half_h * cos_a); - - int x2 = eye_state.x + static_cast(half_w * cos_a + half_h * sin_a); - int y2 = brow_y + static_cast(half_w * sin_a - half_h * cos_a); - - int x3 = eye_state.x + static_cast(half_w * cos_a - half_h * sin_a); - int y3 = brow_y + static_cast(half_w * sin_a + half_h * cos_a); - - int x4 = eye_state.x + static_cast(-half_w * cos_a - half_h * sin_a); - int y4 = brow_y + static_cast(-half_w * sin_a + half_h * cos_a); + lv_point_precise_t p1, p2; + p1.x = eye_state.x - half_w * cos_a; + p1.y = brow_y - half_w * sin_a; + p2.x = eye_state.x + half_w * cos_a; + p2.y = brow_y + half_w * sin_a; lv_layer_t layer; lv_canvas_init_layer(canvas_, &layer); - lv_draw_triangle_dsc_t tri_dsc; - lv_draw_triangle_dsc_init(&tri_dsc); - tri_dsc.color = lv_color_black(); - tri_dsc.opa = LV_OPA_COVER; - - // First triangle: top-left, top-right, bottom-right - tri_dsc.p[0].x = x1; - tri_dsc.p[0].y = y1; - tri_dsc.p[1].x = x2; - tri_dsc.p[1].y = y2; - tri_dsc.p[2].x = x3; - tri_dsc.p[2].y = y3; - lv_draw_triangle(&layer, &tri_dsc); - - // Second triangle: top-left, bottom-right, bottom-left - tri_dsc.p[0].x = x1; - tri_dsc.p[0].y = y1; - tri_dsc.p[1].x = x3; - tri_dsc.p[1].y = y3; - tri_dsc.p[2].x = x4; - tri_dsc.p[2].y = y4; - lv_draw_triangle(&layer, &tri_dsc); + lv_draw_line_dsc_t line_dsc; + lv_draw_line_dsc_init(&line_dsc); + line_dsc.color = lv_color_black(); + line_dsc.width = brow_height; + line_dsc.opa = LV_OPA_COVER; + line_dsc.round_start = 1; + line_dsc.round_end = 1; + line_dsc.p1 = p1; + line_dsc.p2 = p2; + + lv_draw_line(&layer, &line_dsc); lv_canvas_finish_layer(canvas_, &layer); } From a541fa5fb9e068b52d5d4698036828ae4d1f4509 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Jan 2026 08:34:56 -0600 Subject: [PATCH 04/21] minor cleanup --- components/expressive_eyes/example/main/eye_drawer.hpp | 8 +++++--- .../expressive_eyes/example/main/full_featured_drawer.hpp | 8 +++----- .../example/main/monochrome_blue_drawer.hpp | 8 +++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/components/expressive_eyes/example/main/eye_drawer.hpp b/components/expressive_eyes/example/main/eye_drawer.hpp index bd2d57531..8979024ff 100644 --- a/components/expressive_eyes/example/main/eye_drawer.hpp +++ b/components/expressive_eyes/example/main/eye_drawer.hpp @@ -11,11 +11,13 @@ namespace eye_drawer { struct EyeDrawer { virtual ~EyeDrawer() = default; + typedef std::function + DrawCallback; + /// \brief Get the draw callback function /// \return The callback function to be used with ExpressiveEyes - virtual std::function - get_draw_callback() = 0; + virtual DrawCallback get_draw_callback() = 0; /// \brief Clean up resources virtual void cleanup() = 0; diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp index f23b72a7e..2e4b7629f 100755 --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -12,8 +12,8 @@ namespace eye_drawer { /// \details Draws realistic eyes with: /// - White eye background /// - Black pupils with position control -/// - Eyebrows with rotation -/// - Cheeks +/// - Eyebrows with rotation (cut out from the eye) +/// - Cheeks (cut out from the eye) class FullFeaturedDrawer : public EyeDrawer { public: struct Config { @@ -38,9 +38,7 @@ class FullFeaturedDrawer : public EyeDrawer { ~FullFeaturedDrawer() override { cleanup(); } - std::function - get_draw_callback() override { + DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, const espp::ExpressiveEyes::EyeState &right) { std::lock_guard lock(lvgl_mutex_); diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp index e85dd6e60..fbb4c0e8c 100755 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -12,8 +12,8 @@ namespace eye_drawer { /// \details Draws simple eyes with: /// - Electric blue eye shapes (no pupils) /// - Black background -/// - Eyebrows in electric blue -/// - Cheeks in electric blue +/// - Eyebrows in black (cut out from the eye) +/// - Cheeks in black (cut out from eye) class MonochromeBlueDrawer : public EyeDrawer { public: struct Config { @@ -42,9 +42,7 @@ class MonochromeBlueDrawer : public EyeDrawer { ~MonochromeBlueDrawer() override { cleanup(); } - std::function - get_draw_callback() override { + DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, const espp::ExpressiveEyes::EyeState &right) { std::lock_guard lock(lvgl_mutex_); From 599543dd6afc7bde8ddb62bb04070340c6bc2db3 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Jan 2026 11:25:32 -0600 Subject: [PATCH 05/21] update example to go into random mode after test --- .../example/main/expressive_eyes_example.cpp | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/components/expressive_eyes/example/main/expressive_eyes_example.cpp b/components/expressive_eyes/example/main/expressive_eyes_example.cpp index 4fdabefb6..45d4d6ef9 100644 --- a/components/expressive_eyes/example/main/expressive_eyes_example.cpp +++ b/components/expressive_eyes/example/main/expressive_eyes_example.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -191,8 +193,21 @@ extern "C" void app_main(void) { } } - // Back to normal with continuous updates - logger.info("Expression: Normal (continuous loop)"); + // Random demo mode - continuously looks around and changes expressions + logger.info("Starting random demo mode - will run continuously"); + + // Seed random number generator + srand(time(nullptr)); + + // Reset to neutral + eyes.set_expression(espp::ExpressiveEyes::Expression::NEUTRAL); + eyes.look_at(0.0f, 0.0f); + + // Random mode state + float time_until_next_look = 2.0f + (rand() % 4000) / 1000.0f; // 2-6 seconds + float time_until_next_expression = 5.0f + (rand() % 10000) / 1000.0f; // 5-15 seconds + float look_timer = 0.0f; + float expression_timer = 0.0f; // Animation loop last_time = std::chrono::steady_clock::now(); @@ -201,6 +216,42 @@ extern "C" void app_main(void) { float dt = std::chrono::duration(now - last_time).count(); last_time = now; + // Update timers + look_timer += dt; + expression_timer += dt; + + // Randomly look around + if (look_timer >= time_until_next_look) { + float look_x = ((rand() % 2000) - 1000) / 1000.0f; // -1.0 to 1.0 + float look_y = ((rand() % 2000) - 1000) / 1000.0f; // -1.0 to 1.0 + eyes.look_at(look_x, look_y); + look_timer = 0.0f; + time_until_next_look = 2.0f + (rand() % 4000) / 1000.0f; // 2-6 seconds + } + + // Randomly change expression (weighted toward neutral) + if (expression_timer >= time_until_next_expression) { + int expr_choice = rand() % 10; + if (expr_choice < 5) { + // 50% chance: stay neutral + eyes.set_expression(espp::ExpressiveEyes::Expression::NEUTRAL); + } else if (expr_choice < 7) { + // 20% chance: happy + eyes.set_expression(espp::ExpressiveEyes::Expression::HAPPY); + } else if (expr_choice < 8) { + // 10% chance: surprised + eyes.set_expression(espp::ExpressiveEyes::Expression::SURPRISED); + } else if (expr_choice < 9) { + // 10% chance: sad + eyes.set_expression(espp::ExpressiveEyes::Expression::SAD); + } else { + // 10% chance: angry + eyes.set_expression(espp::ExpressiveEyes::Expression::ANGRY); + } + expression_timer = 0.0f; + time_until_next_expression = 5.0f + (rand() % 10000) / 1000.0f; // 5-15 seconds + } + // Update and render eyes (calls draw callback) eyes.update(dt); From 3bd18eb9294de8680bb5c23a2b9c7b537b2a1189 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Jan 2026 22:28:03 -0600 Subject: [PATCH 06/21] finalizing code and adding docs / ci --- .github/workflows/build.yml | 2 + .github/workflows/upload_components.yml | 1 + components/expressive_eyes/CMakeLists.txt | 4 +- components/expressive_eyes/README.md | 96 +++----------- components/expressive_eyes/example/README.md | 122 +++++++++++------- .../example/main/README_DRAWERS.md | 6 +- .../example/main/expressive_eyes_example.cpp | 18 +-- .../example/main/eye_drawer.hpp | 29 ++++- .../example/main/full_featured_drawer.hpp | 19 ++- .../example/main/monochrome_blue_drawer.hpp | 18 ++- components/expressive_eyes/idf_component.yml | 8 +- doc/Doxyfile | 2 + doc/en/display/index.rst | 1 + doc/en/expressive_eyes.rst | 38 ++++++ doc/en/expressive_eyes_example.md | 2 + 15 files changed, 222 insertions(+), 144 deletions(-) mode change 100644 => 100755 .github/workflows/build.yml mode change 100644 => 100755 .github/workflows/upload_components.yml mode change 100644 => 100755 components/expressive_eyes/README.md mode change 100644 => 100755 components/expressive_eyes/example/README.md mode change 100644 => 100755 doc/Doxyfile mode change 100644 => 100755 doc/en/display/index.rst create mode 100755 doc/en/expressive_eyes.rst create mode 100755 doc/en/expressive_eyes_example.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml old mode 100644 new mode 100755 index 5db1612a7..cb96a2d34 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,8 @@ jobs: target: esp32s3 - path: 'components/event_manager/example' target: esp32 + - path: 'components/expressive_eyes/example' + target: esp32s3 - path: 'components/file_system/example' target: esp32 - path: 'components/filters/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml old mode 100644 new mode 100755 index 7b3307f85..ca4126141 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -59,6 +59,7 @@ jobs: components/esp-box components/esp32-timer-cam components/event_manager + components/expressive_eyes components/file_system components/filters components/format diff --git a/components/expressive_eyes/CMakeLists.txt b/components/expressive_eyes/CMakeLists.txt index ca7cc6e15..f8bef1050 100644 --- a/components/expressive_eyes/CMakeLists.txt +++ b/components/expressive_eyes/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "src/expressive_eyes.cpp" INCLUDE_DIRS "include" - REQUIRES logger base_component + SRC_DIRS "src" + REQUIRES base_component ) diff --git a/components/expressive_eyes/README.md b/components/expressive_eyes/README.md old mode 100644 new mode 100755 index 2d8ced90b..4cd22a9b6 --- a/components/expressive_eyes/README.md +++ b/components/expressive_eyes/README.md @@ -1,86 +1,26 @@ # Expressive Eyes Component -Animated expressive eyes for displays using simple blob shapes. +[![Badge](https://components.espressif.com/components/espp/expressive_eyes/badge.svg)](https://components.espressif.com/components/espp/expressive_eyes) -## Features - -- Multiple expressions (happy, sad, angry, surprised, sleepy, wink) -- Smooth eye movement and blinking -- Physics-based pupil movement -- Customizable appearance -- Frame-based animation system -- Automatic random blinking - -## Usage - -```cpp -//! [expressive eyes example] -#include "expressive_eyes.hpp" -#include "display.hpp" // Your display driver +The `ExpressiveEyes` component provides animated expressive eyes for displays +using simple blob shapes. Eyes can blink, look around, change expression, and +display various emotions with smooth animations. -// Drawing callback -auto draw_eye = [](int x, int y, int width, int height, float rotation, - uint16_t color, bool has_pupil, float pupil_x, float pupil_y) { - // Draw eye ellipse - display.fill_ellipse(x, y, width/2, height/2, color); - - if (has_pupil && height > 10) { - // Calculate pupil position - int px = x + static_cast(pupil_x * width * 0.3f); - int py = y + static_cast(pupil_y * height * 0.3f); - display.fill_circle(px, py, 10, 0x0000); // Black pupil - } -}; +The component uses a callback-based drawing system, allowing you to implement +custom renderers for different display types and visual styles. -// Configure eyes -espp::ExpressiveEyes::Config config{ - .screen_width = 320, - .screen_height = 240, - .eye_spacing = 100, - .eye_width = 60, - .eye_height = 80, - .pupil_size = 20, - .enable_auto_blink = true, - .enable_pupil_physics = true, - .on_draw = draw_eye -}; - -espp::ExpressiveEyes eyes(config); - -// Animation loop -auto last_time = std::chrono::steady_clock::now(); -while (true) { - auto now = std::chrono::steady_clock::now(); - float dt = std::chrono::duration(now - last_time).count(); - last_time = now; - - eyes.update(dt); - - // Change expression based on input - eyes.set_expression(espp::ExpressiveEyes::Expression::HAPPY); - - // Look at position - eyes.look_at(0.5f, -0.3f); // Look up-right - - std::this_thread::sleep_for(16ms); // ~60 FPS -} -//! [expressive eyes example] -``` - -## Expressions +## Features -- `NEUTRAL` - Normal open eyes -- `HAPPY` - Squinted happy eyes -- `SAD` - Droopy sad eyes -- `ANGRY` - Angled angry eyebrows -- `SURPRISED` - Wide open eyes -- `SLEEPY` - Half-closed eyes -- `WINK_LEFT` - Left eye closed -- `WINK_RIGHT` - Right eye closed +- Multiple expressions (happy, sad, angry, surprised, neutral, sleepy, wink) +- Smooth eye movement with look_at positioning +- Automatic blinking with configurable intervals +- Optional pupils with physics-based movement +- Eyebrows and cheeks for enhanced expressions +- Customizable colors and sizes +- Frame-based animation system -## Customization +## Example -- Adjust `eye_width` and `eye_height` for different eye shapes -- Set `pupil_size = 0` to disable pupils -- Modify `blink_interval` to change blink frequency -- Disable `enable_pupil_physics` for instant pupil movement +The [example](./example) demonstrates the expressive eyes component with two +different drawer implementations (full-featured realistic eyes and monochrome +blue eyes) running on various ESP32 display boards. diff --git a/components/expressive_eyes/example/README.md b/components/expressive_eyes/example/README.md old mode 100644 new mode 100755 index a0af1e80d..baed6fa51 --- a/components/expressive_eyes/example/README.md +++ b/components/expressive_eyes/example/README.md @@ -1,68 +1,96 @@ # Expressive Eyes Example -Demonstrates animated expressive eyes on various display boards. +This example demonstrates animated expressive eyes on various ESP32 display +boards. It showcases different expressions, eye movements, and includes a +continuous random demo mode perfect for a desk display. -## Hardware Required +## How to use example -Select one of the supported boards in menuconfig: +### Hardware Required + +This example can be configured to run on the following dev boards: - ESP32-S3-BOX / ESP32-S3-BOX-3 - MaTouch Rotary Display -- ESP-WROVER-KIT -- M5Stack Tab5 - -## How to Use - -### Hardware Setup +- WS-S3-Touch -Connect your chosen display board. No additional wiring is needed. +### Configure the project -### Configure the Project - -```bash +``` idf.py menuconfig ``` -Navigate to `Expressive Eyes Example Configuration`: +Navigate to `Expressive Eyes Example Configuration` to: - Select your board -- Configure eye appearance (size, spacing, color) -- Enable/disable auto demo mode -- Configure blink settings -- Enable/disable pupils +- Choose drawing method (Full Featured or Monochrome Blue) ### Build and Flash -```bash -idf.py build flash monitor +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor ``` -## Features +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. -### Auto Demo Mode (Enabled by default) +## Example Breakdown -Automatically cycles through different expressions every 3 seconds (configurable): +The example has three phases: + +### 1. Expression Showcase + +Cycles through all available expressions (3 seconds each): - Neutral - Happy - Sad - Angry - Surprised -- Sleepy -- Wink Left -- Wink Right -Eyes also randomly look around and blink automatically. +### 2. Look Direction Demo + +Demonstrates the look_at functionality by looking in different directions (1.5 seconds each): +- Left +- Right +- Up +- Down +- Center + +### 3. Random Demo Mode + +Enters continuous random demo mode for engaging desk display: +- **Natural behavior**: Mostly stays neutral with occasional emotional expressions +- **Random looking**: Eyes look in random directions every 2-6 seconds +- **Expression changes**: Randomly changes expression every 5-15 seconds + - 50% neutral (most common) + - 20% happy + - 10% surprised + - 10% sad + - 10% angry +- **Automatic blinking**: Eyes blink naturally at random intervals -### Manual Mode +The random demo mode runs indefinitely, making it perfect for a continuous desk display. -Disable auto demo to show a single expression. Modify the code to control expressions programmatically. +## Drawer Implementations -### Customization +The example includes two drawer implementations: -All parameters can be adjusted via menuconfig without code changes: -- Eye dimensions and spacing -- Pupil size (or disable pupils entirely) -- Blink frequency -- Demo interval -- Eye color (RGB565) +### Full Featured Drawer +- White eye background with black pupils +- Eyebrows drawn as rotated lines with rounded caps +- Cheeks for emotional expressions +- Respects LVGL theme background color + +### Monochrome Blue Drawer +- Electric blue (#00BFFF) eyes on black background +- No pupils (solid colored eyes) +- Minimalist aesthetic perfect for sci-fi interfaces +- Black eyebrows and cheeks + +See [README_DRAWERS.md](./main/README_DRAWERS.md) for details on creating custom drawer implementations. ## Example Output @@ -70,13 +98,17 @@ All parameters can be adjusted via menuconfig without code changes: I (380) Expressive Eyes Example: Starting Expressive Eyes Example I (425) Expressive Eyes Example: Display size: 320x240 I (430) Expressive Eyes Example: Expressive eyes initialized -I (435) Expressive Eyes Example: Starting auto demo mode (interval: 3000ms) -I (3440) Expressive Eyes Example: Changed to expression 1, looking at (0.42, -0.67) -I (6445) Expressive Eyes Example: Changed to expression 2, looking at (-0.23, 0.81) +I (435) Expressive Eyes Example: Testing different expressions... +I (440) Expressive Eyes Example: Expression: NEUTRAL +I (3445) Expressive Eyes Example: Expression: HAPPY +I (6450) Expressive Eyes Example: Expression: SAD +I (9455) Expressive Eyes Example: Expression: ANGRY +I (12460) Expressive Eyes Example: Expression: SURPRISED +I (15465) Expressive Eyes Example: Testing look_at functionality +I (15470) Expressive Eyes Example: Looking left +I (16975) Expressive Eyes Example: Looking right +I (18480) Expressive Eyes Example: Looking up +I (19985) Expressive Eyes Example: Looking down +I (21490) Expressive Eyes Example: Looking center +I (22995) Expressive Eyes Example: Starting random demo mode - will run continuously ``` - -## Troubleshooting - -- **Display not working**: Check board selection in menuconfig -- **Eyes too small/large**: Adjust eye width/height in menuconfig -- **Performance issues**: Try reducing eye size or disabling pupils diff --git a/components/expressive_eyes/example/main/README_DRAWERS.md b/components/expressive_eyes/example/main/README_DRAWERS.md index d42aa18c9..b18a1583d 100755 --- a/components/expressive_eyes/example/main/README_DRAWERS.md +++ b/components/expressive_eyes/example/main/README_DRAWERS.md @@ -17,7 +17,7 @@ The drawer system uses a base interface (`EyeDrawer`) with concrete implementati Draws realistic-looking eyes with: - White eye background - Black pupils with position tracking -- Eyebrows with rotation support +- Eyebrows with rotation support (using lv_draw_line for smooth rendering) - Cheeks for emotional expressions - Uses background color from LVGL theme @@ -29,7 +29,7 @@ Draws minimalist electric blue eyes with: - Electric blue (#00BFFF) eye shapes - Black background - No pupils (solid colored eyes) -- Black eyebrows and cheeks (cut out from eyes as negative space) +- Black eyebrows and cheeks (using lv_draw_line with rounded caps) - Clean, modern aesthetic **Best for**: Sci-fi/futuristic interfaces, minimal power displays, WS-S3-Touch boards @@ -100,3 +100,5 @@ public: - Use `eye_state.height / original_eye_height` to detect blink state - Mirror eyebrow angles for left vs right eyes - Use `screen_width` and `screen_height` for relative positioning +- For eyebrows, use `lv_draw_line` with width parameter instead of triangles for better performance and no seam artifacts +- Set `round_start` and `round_end` to 1 for smooth rounded line caps diff --git a/components/expressive_eyes/example/main/expressive_eyes_example.cpp b/components/expressive_eyes/example/main/expressive_eyes_example.cpp index 45d4d6ef9..f3d9cdc01 100644 --- a/components/expressive_eyes/example/main/expressive_eyes_example.cpp +++ b/components/expressive_eyes/example/main/expressive_eyes_example.cpp @@ -147,6 +147,7 @@ extern "C" void app_main(void) { // Test different expressions using array iteration logger.info("Testing different expressions..."); + // Cycle through each expression preset to demonstrate different emotional states const espp::ExpressiveEyes::Expression expressions[] = { espp::ExpressiveEyes::Expression::NEUTRAL, espp::ExpressiveEyes::Expression::HAPPY, espp::ExpressiveEyes::Expression::SAD, espp::ExpressiveEyes::Expression::ANGRY, @@ -169,10 +170,11 @@ extern "C" void app_main(void) { logger.info("Testing look_at functionality"); eyes.set_expression(espp::ExpressiveEyes::Expression::NEUTRAL); + // Demonstrate directional looking in the 4 cardinal directions plus center struct LookDirection { const char *name; - float x; - float y; + float x; ///< Horizontal direction: -1.0 = left, 1.0 = right, 0 = center + float y; ///< Vertical direction: -1.0 = up, 1.0 = down, 0 = center }; const LookDirection look_directions[] = {{"left", -1.0f, 0.0f}, @@ -196,20 +198,20 @@ extern "C" void app_main(void) { // Random demo mode - continuously looks around and changes expressions logger.info("Starting random demo mode - will run continuously"); - // Seed random number generator + // Seed random number generator with current time srand(time(nullptr)); - // Reset to neutral + // Reset to neutral expression and center gaze eyes.set_expression(espp::ExpressiveEyes::Expression::NEUTRAL); eyes.look_at(0.0f, 0.0f); - // Random mode state + // Random mode state - tracks when to trigger next action float time_until_next_look = 2.0f + (rand() % 4000) / 1000.0f; // 2-6 seconds float time_until_next_expression = 5.0f + (rand() % 10000) / 1000.0f; // 5-15 seconds float look_timer = 0.0f; float expression_timer = 0.0f; - // Animation loop + // Animation loop - runs indefinitely for continuous desk display last_time = std::chrono::steady_clock::now(); while (true) { auto now = std::chrono::steady_clock::now(); @@ -220,7 +222,7 @@ extern "C" void app_main(void) { look_timer += dt; expression_timer += dt; - // Randomly look around + // Randomly look around - gives eyes natural movement if (look_timer >= time_until_next_look) { float look_x = ((rand() % 2000) - 1000) / 1000.0f; // -1.0 to 1.0 float look_y = ((rand() % 2000) - 1000) / 1000.0f; // -1.0 to 1.0 @@ -229,7 +231,7 @@ extern "C" void app_main(void) { time_until_next_look = 2.0f + (rand() % 4000) / 1000.0f; // 2-6 seconds } - // Randomly change expression (weighted toward neutral) + // Randomly change expression (weighted toward neutral for natural behavior) if (expression_timer >= time_until_next_expression) { int expr_choice = rand() % 10; if (expr_choice < 5) { diff --git a/components/expressive_eyes/example/main/eye_drawer.hpp b/components/expressive_eyes/example/main/eye_drawer.hpp index 8979024ff..46782664b 100644 --- a/components/expressive_eyes/example/main/eye_drawer.hpp +++ b/components/expressive_eyes/example/main/eye_drawer.hpp @@ -7,19 +7,40 @@ namespace eye_drawer { -/// \brief Base interface for eye drawing implementations +/** + * @brief Base interface for eye drawing implementations + * + * Defines the interface that all eye drawer implementations must follow. + * Drawer implementations receive eye state data from ExpressiveEyes and + * are responsible for rendering the eyes using their chosen graphics API. + * + * @note Implementations should use thread-safe operations when drawing + * (e.g., lock LVGL mutex before LVGL calls). + */ struct EyeDrawer { virtual ~EyeDrawer() = default; + /** + * @brief Callback function type for drawing eyes + * @param left_eye Left eye state data + * @param right_eye Right eye state data + */ typedef std::function DrawCallback; - /// \brief Get the draw callback function - /// \return The callback function to be used with ExpressiveEyes + /** + * @brief Get the draw callback function + * @return The callback function to be used with ExpressiveEyes + */ virtual DrawCallback get_draw_callback() = 0; - /// \brief Clean up resources + /** + * @brief Clean up resources + * + * Called when the drawer is being destroyed. Implementations should + * free any allocated resources. + */ virtual void cleanup() = 0; }; diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp index 2e4b7629f..98a62a61a 100755 --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -24,6 +24,10 @@ class FullFeaturedDrawer : public EyeDrawer { std::recursive_mutex &lvgl_mutex; }; + /** + * @brief Construct full-featured drawer + * @param config Configuration structure + */ explicit FullFeaturedDrawer(const Config &config) : screen_width_(config.screen_width) , screen_height_(config.screen_height) @@ -60,7 +64,14 @@ class FullFeaturedDrawer : public EyeDrawer { } private: - // Helper to draw filled ellipse (for eyes) using layer API + /** + * @brief Draw a filled ellipse using LVGL layer API + * @param cx Center X coordinate + * @param cy Center Y coordinate + * @param width Ellipse width + * @param height Ellipse height + * @param color Fill color + */ void draw_ellipse(int cx, int cy, int width, int height, lv_color_t color) { lv_layer_t layer; lv_canvas_init_layer(canvas_, &layer); @@ -82,6 +93,12 @@ class FullFeaturedDrawer : public EyeDrawer { lv_canvas_finish_layer(canvas_, &layer); } + /** + * @brief Draw a single eye with all components + * @param eye_state Eye state data from ExpressiveEyes + * @param is_left True if left eye, false if right eye (affects eyebrow mirroring) + * @param bg_color Background color for eyebrows and cheeks + */ void draw_single_eye(const espp::ExpressiveEyes::EyeState &eye_state, bool is_left, lv_color_t bg_color) { // Draw eye white diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp index fbb4c0e8c..a99bed0d5 100755 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -24,6 +24,10 @@ class MonochromeBlueDrawer : public EyeDrawer { std::recursive_mutex &lvgl_mutex; }; + /** + * @brief Construct monochrome blue drawer + * @param config Configuration structure + */ explicit MonochromeBlueDrawer(const Config &config) : screen_width_(config.screen_width) , screen_height_(config.screen_height) @@ -61,7 +65,14 @@ class MonochromeBlueDrawer : public EyeDrawer { } private: - // Helper to draw filled ellipse using layer API + /** + * @brief Draw a filled ellipse using LVGL layer API + * @param cx Center X coordinate + * @param cy Center Y coordinate + * @param width Ellipse width + * @param height Ellipse height + * @param color Fill color + */ void draw_ellipse(int cx, int cy, int width, int height, lv_color_t color) { lv_layer_t layer; lv_canvas_init_layer(canvas_, &layer); @@ -83,6 +94,11 @@ class MonochromeBlueDrawer : public EyeDrawer { lv_canvas_finish_layer(canvas_, &layer); } + /** + * @brief Draw a single eye with all components + * @param eye_state Eye state data from ExpressiveEyes + * @param is_left True if left eye, false if right eye (affects eyebrow mirroring) + */ void draw_single_eye(const espp::ExpressiveEyes::EyeState &eye_state, bool is_left) { // Draw eye in electric blue (no pupil) draw_ellipse(eye_state.x, eye_state.y, eye_state.width, eye_state.height, electric_blue_); diff --git a/components/expressive_eyes/idf_component.yml b/components/expressive_eyes/idf_component.yml index e9b3fe0cb..c9f803597 100644 --- a/components/expressive_eyes/idf_component.yml +++ b/components/expressive_eyes/idf_component.yml @@ -5,13 +5,15 @@ url: "https://github.com/esp-cpp/espp/tree/main/components/expressive_eyes" repository: "git://github.com/esp-cpp/espp.git" maintainers: - William Emfinger -documentation: "https://esp-cpp.github.io/espp/expressive_eyes.html" +documentation: "https://esp-cpp.github.io/espp/display/expressive_eyes.html" tags: - cpp - Animation - Display + - Expressive + - Eyes + - Face dependencies: idf: version: '>=5.0' - espp/logger: '>=1.0' - espp/task: '>=1.0' + espp/base_component: '>=1.0' diff --git a/doc/Doxyfile b/doc/Doxyfile old mode 100644 new mode 100755 index 80d233967..ad2f7067e --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -96,6 +96,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/esp32-timer-cam/example/main/esp_timer_cam_example.cpp \ $(PROJECT_PATH)/components/esp-box/example/main/esp_box_example.cpp \ $(PROJECT_PATH)/components/event_manager/example/main/event_manager_example.cpp \ + $(PROJECT_PATH)/components/expressive_eyes/example/main/expressive_eyes_example.cpp \ $(PROJECT_PATH)/components/file_system/example/main/file_system_example.cpp \ $(PROJECT_PATH)/components/filters/example/main/filters_example.cpp \ $(PROJECT_PATH)/components/ftp/example/main/ftp_example.cpp \ @@ -219,6 +220,7 @@ INPUT = \ $(PROJECT_PATH)/components/esp32-timer-cam/include/esp32-timer-cam.hpp \ $(PROJECT_PATH)/components/esp-box/include/esp-box.hpp \ $(PROJECT_PATH)/components/event_manager/include/event_manager.hpp \ + $(PROJECT_PATH)/components/expressive_eyes/include/expressive_eyes.hpp \ $(PROJECT_PATH)/components/file_system/include/file_system.hpp \ $(PROJECT_PATH)/components/filters/include/biquad_filter.hpp \ $(PROJECT_PATH)/components/filters/include/butterworth_filter.hpp \ diff --git a/doc/en/display/index.rst b/doc/en/display/index.rst old mode 100644 new mode 100755 index 919c1a8bd..80b9fbdb5 --- a/doc/en/display/index.rst +++ b/doc/en/display/index.rst @@ -6,6 +6,7 @@ Display APIs display display_drivers + expressive_eyes Code examples for the display API are provided in the `display_drivers` example folder.. diff --git a/doc/en/expressive_eyes.rst b/doc/en/expressive_eyes.rst new file mode 100755 index 000000000..80d31be56 --- /dev/null +++ b/doc/en/expressive_eyes.rst @@ -0,0 +1,38 @@ +Expressive Eyes APIs +******************** + +The `ExpressiveEyes` class provides an animated expressive eyes system for +displays using simple blob shapes. It supports multiple expressions, smooth +eye movement, blinking, and optional physics-based pupil movement. + +The component uses a callback-based drawing system, allowing you to implement +custom renderers for different display types and visual styles. Example drawer +implementations are provided showing both realistic eyes with pupils and +minimalist monochrome designs. + +Features include: + +- Multiple expressions (happy, sad, angry, surprised, neutral) +- Smooth eye movement with look_at positioning +- Automatic blinking with configurable intervals +- Optional pupils with physics-based movement +- Eyebrows and cheeks for enhanced expressions +- Customizable colors and sizes +- Frame-based animation system + +The drawing callback receives complete eye state information including +position, size, expression parameters, pupil position, eyebrow configuration, +and more, giving you full control over the rendering. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + expressive_eyes_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/expressive_eyes.inc diff --git a/doc/en/expressive_eyes_example.md b/doc/en/expressive_eyes_example.md new file mode 100755 index 000000000..4c8d75979 --- /dev/null +++ b/doc/en/expressive_eyes_example.md @@ -0,0 +1,2 @@ +```{include} ../../components/expressive_eyes/example/README.md +``` From 6f4c53331ef8ae050a7a7d3f142cda7c2e14b6f4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Jan 2026 22:30:46 -0600 Subject: [PATCH 07/21] fix doc --- doc/en/{ => display}/expressive_eyes.rst | 0 doc/en/display/expressive_eyes_example.md | 2 ++ doc/en/expressive_eyes_example.md | 2 -- 3 files changed, 2 insertions(+), 2 deletions(-) rename doc/en/{ => display}/expressive_eyes.rst (100%) create mode 100755 doc/en/display/expressive_eyes_example.md delete mode 100755 doc/en/expressive_eyes_example.md diff --git a/doc/en/expressive_eyes.rst b/doc/en/display/expressive_eyes.rst similarity index 100% rename from doc/en/expressive_eyes.rst rename to doc/en/display/expressive_eyes.rst diff --git a/doc/en/display/expressive_eyes_example.md b/doc/en/display/expressive_eyes_example.md new file mode 100755 index 000000000..afd9b8596 --- /dev/null +++ b/doc/en/display/expressive_eyes_example.md @@ -0,0 +1,2 @@ +```{include} ../../../components/expressive_eyes/example/README.md +``` diff --git a/doc/en/expressive_eyes_example.md b/doc/en/expressive_eyes_example.md deleted file mode 100755 index 4c8d75979..000000000 --- a/doc/en/expressive_eyes_example.md +++ /dev/null @@ -1,2 +0,0 @@ -```{include} ../../components/expressive_eyes/example/README.md -``` From ec5655baefeb62fb695e0b7a4b0a12a5f0c8b459 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Jan 2026 22:35:43 -0600 Subject: [PATCH 08/21] add console output --- components/expressive_eyes/example/README.md | 46 ++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/components/expressive_eyes/example/README.md b/components/expressive_eyes/example/README.md index baed6fa51..13b51a1ae 100755 --- a/components/expressive_eyes/example/README.md +++ b/components/expressive_eyes/example/README.md @@ -94,21 +94,33 @@ See [README_DRAWERS.md](./main/README_DRAWERS.md) for details on creating custom ## Example Output -``` -I (380) Expressive Eyes Example: Starting Expressive Eyes Example -I (425) Expressive Eyes Example: Display size: 320x240 -I (430) Expressive Eyes Example: Expressive eyes initialized -I (435) Expressive Eyes Example: Testing different expressions... -I (440) Expressive Eyes Example: Expression: NEUTRAL -I (3445) Expressive Eyes Example: Expression: HAPPY -I (6450) Expressive Eyes Example: Expression: SAD -I (9455) Expressive Eyes Example: Expression: ANGRY -I (12460) Expressive Eyes Example: Expression: SURPRISED -I (15465) Expressive Eyes Example: Testing look_at functionality -I (15470) Expressive Eyes Example: Looking left -I (16975) Expressive Eyes Example: Looking right -I (18480) Expressive Eyes Example: Looking up -I (19985) Expressive Eyes Example: Looking down -I (21490) Expressive Eyes Example: Looking center -I (22995) Expressive Eyes Example: Starting random demo mode - will run continuously +image + +```console +I (886) main_task: Calling app_main() +[Expressive Eyes Example/I][0.889]: Starting Expressive Eyes Example +[WsS3Touch/I][0.899]: Initializing LCD... +[WsS3Touch/I][0.900]: Initializing SPI... +[WsS3Touch/I][0.906]: Adding device to SPI bus... +[WsS3Touch/I][0.911]: Initializing the display driver... +[WsS3Touch/I][1.321]: Display driver initialized successfully! +W (1322) ledc: the binded timer can't keep alive in sleep +[WsS3Touch/I][1.324]: Initializing display with pixel buffer size: 12000 +[WsS3Touch/I][1.334]: Display initialized successfully! +[Expressive Eyes Example/I][1.335]: Display size: 240x280 +[Expressive Eyes Example/I][1.342]: Using Monochrome Blue drawer +[Expressive Eyes Example/I][1.392]: Expressive eyes initialized +[Expressive Eyes Example/I][1.392]: Testing different expressions... +[Expressive Eyes Example/I][1.395]: Expression: Neutral +[Expressive Eyes Example/I][11.928]: Expression: Happy +[Expressive Eyes Example/I][22.548]: Expression: Sad +[Expressive Eyes Example/I][33.168]: Expression: Angry +[Expressive Eyes Example/I][43.788]: Expression: Surprised +[Expressive Eyes Example/I][54.407]: Testing look_at functionality +[Expressive Eyes Example/I][54.408]: Looking left +[Expressive Eyes Example/I][59.717]: Looking right +[Expressive Eyes Example/I][65.028]: Looking up +[Expressive Eyes Example/I][70.337]: Looking down +[Expressive Eyes Example/I][75.648]: Looking center +[Expressive Eyes Example/I][80.957]: Starting random demo mode - will run continuously ``` From 433bbf8ff35dca57a9a31e1b7faf35c858772428 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sat, 24 Jan 2026 13:51:21 -0600 Subject: [PATCH 09/21] readme: update --- components/expressive_eyes/README.md | 19 ++++++++++ components/expressive_eyes/example/README.md | 39 ++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/components/expressive_eyes/README.md b/components/expressive_eyes/README.md index 4cd22a9b6..11ec62cc7 100755 --- a/components/expressive_eyes/README.md +++ b/components/expressive_eyes/README.md @@ -19,6 +19,25 @@ custom renderers for different display types and visual styles. - Customizable colors and sizes - Frame-based animation system + + + + + + + + + + + + + +
happy imagesad image
angry imagesurprised image
looking imagelooking image
+ +Videos: + +https://github.com/user-attachments/assets/42f5db22-1b2b-4a66-945f-542e57df55bf + ## Example The [example](./example) demonstrates the expressive eyes component with two diff --git a/components/expressive_eyes/example/README.md b/components/expressive_eyes/example/README.md index 13b51a1ae..a78f10a68 100755 --- a/components/expressive_eyes/example/README.md +++ b/components/expressive_eyes/example/README.md @@ -4,6 +4,25 @@ This example demonstrates animated expressive eyes on various ESP32 display boards. It showcases different expressions, eye movements, and includes a continuous random demo mode perfect for a desk display. + + + + + + + + + + + + + +
happy imagesad image
angry imagesurprised image
looking imagelooking image
+ +Videos: + +https://github.com/user-attachments/assets/42f5db22-1b2b-4a66-945f-542e57df55bf + ## How to use example ### Hardware Required @@ -94,6 +113,26 @@ See [README_DRAWERS.md](./main/README_DRAWERS.md) for details on creating custom ## Example Output + + + + + + + + + + + + + +
happy imagesad image
angry imagesurprised image
looking imagelooking image
+ +Videos: + +https://github.com/user-attachments/assets/42f5db22-1b2b-4a66-945f-542e57df55bf + + image ```console From 8e3a541320941f7a4af9e75f185ee1a3234b916b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sat, 24 Jan 2026 16:16:48 -0600 Subject: [PATCH 10/21] address sa --- .../example/main/full_featured_drawer.hpp | 8 ++++---- .../example/main/monochrome_blue_drawer.hpp | 17 +++++++---------- .../expressive_eyes/src/expressive_eyes.cpp | 2 -- 3 files changed, 11 insertions(+), 16 deletions(-) mode change 100755 => 100644 components/expressive_eyes/example/main/full_featured_drawer.hpp mode change 100755 => 100644 components/expressive_eyes/example/main/monochrome_blue_drawer.hpp diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp old mode 100755 new mode 100644 index 98a62a61a..cbdd82aca --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -40,9 +40,9 @@ class FullFeaturedDrawer : public EyeDrawer { pupil_size_ = static_cast(std::min(eye_base_width_, original_eye_height_) * 0.3f); } - ~FullFeaturedDrawer() override { cleanup(); } + virtual ~FullFeaturedDrawer() { cleanup(); } - DrawCallback get_draw_callback() override { + virtual DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, const espp::ExpressiveEyes::EyeState &right) { std::lock_guard lock(lvgl_mutex_); @@ -59,8 +59,8 @@ class FullFeaturedDrawer : public EyeDrawer { }; } - void cleanup() override { - // Nothing to clean up in this implementation + virtual void cleanup() override { + // No dynamic resources to free } private: diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp old mode 100755 new mode 100644 index a99bed0d5..15ed64b93 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -19,6 +19,7 @@ class MonochromeBlueDrawer : public EyeDrawer { struct Config { int screen_width; int screen_height; + lv_color_t color = lv_color_hex(0x00BFFF); // Electric blue color lv_obj_t *canvas; lv_color_t *canvas_buffer; std::recursive_mutex &lvgl_mutex; @@ -33,20 +34,16 @@ class MonochromeBlueDrawer : public EyeDrawer { , screen_height_(config.screen_height) , canvas_(config.canvas) , canvas_buffer_(config.canvas_buffer) - , lvgl_mutex_(config.lvgl_mutex) { + , lvgl_mutex_(config.lvgl_mutex) + , electric_blue_(config.color) { // Calculate eye dimensions original_eye_height_ = screen_height_ * 0.55f; eye_base_width_ = screen_width_ * 0.35f; - - // Electric blue color - RGB565 format - // R: 0x00 (0), G: 0xBF (191 -> 23 in 6-bit), B: 0xFF (255 -> 31 in 5-bit) - // RGB565: RRRRRGGGGGGBBBBB = 00000101111111111 = 0x05FF - electric_blue_ = lv_color_hex(0x00BFFF); } - ~MonochromeBlueDrawer() override { cleanup(); } + virtual ~MonochromeBlueDrawer() { cleanup(); } - DrawCallback get_draw_callback() override { + virtual DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, const espp::ExpressiveEyes::EyeState &right) { std::lock_guard lock(lvgl_mutex_); @@ -60,8 +57,8 @@ class MonochromeBlueDrawer : public EyeDrawer { }; } - void cleanup() override { - // Nothing to clean up in this implementation + virtual void cleanup() override { + // No dynamic resources to free } private: diff --git a/components/expressive_eyes/src/expressive_eyes.cpp b/components/expressive_eyes/src/expressive_eyes.cpp index ab52aeee4..c7708eb26 100755 --- a/components/expressive_eyes/src/expressive_eyes.cpp +++ b/components/expressive_eyes/src/expressive_eyes.cpp @@ -144,8 +144,6 @@ void ExpressiveEyes::blend_expression_states(ExpressionState &result, const Expr result.eyebrow.height = lerp(from.eyebrow.height, to.eyebrow.height, t); result.eyebrow.thickness = lerp(from.eyebrow.thickness, to.eyebrow.thickness, t); - // Blend cheek - result.cheek.enabled = from.cheek.enabled || to.cheek.enabled; // Cheek enabled is binary, use the target state result.cheek.enabled = t < 0.5f ? from.cheek.enabled : to.cheek.enabled; From 6a9b7c6516b6882c207fbe8a435fc9241f21bfec Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 11:18:12 -0600 Subject: [PATCH 11/21] fix sa --- .../expressive_eyes/example/main/full_featured_drawer.hpp | 2 +- .../expressive_eyes/example/main/monochrome_blue_drawer.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp index cbdd82aca..09597f008 100644 --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -40,7 +40,7 @@ class FullFeaturedDrawer : public EyeDrawer { pupil_size_ = static_cast(std::min(eye_base_width_, original_eye_height_) * 0.3f); } - virtual ~FullFeaturedDrawer() { cleanup(); } + virtual ~FullFeaturedDrawer() override { cleanup(); } virtual DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp index 15ed64b93..53538e5ec 100644 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -41,7 +41,7 @@ class MonochromeBlueDrawer : public EyeDrawer { eye_base_width_ = screen_width_ * 0.35f; } - virtual ~MonochromeBlueDrawer() { cleanup(); } + virtual ~MonochromeBlueDrawer() override { cleanup(); } virtual DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, From a089716b67929fb5f723b40b273a402effe0aabe Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 11:23:01 -0600 Subject: [PATCH 12/21] simplify config --- .../example/main/Kconfig.projbuild | 68 ------------------- .../example/sdkconfig.defaults | 12 ---- 2 files changed, 80 deletions(-) diff --git a/components/expressive_eyes/example/main/Kconfig.projbuild b/components/expressive_eyes/example/main/Kconfig.projbuild index 8a6ea6087..a5cdd75c9 100755 --- a/components/expressive_eyes/example/main/Kconfig.projbuild +++ b/components/expressive_eyes/example/main/Kconfig.projbuild @@ -42,72 +42,4 @@ menu "Expressive Eyes Example Configuration" endchoice - config EXPRESSIVE_EYES_AUTO_DEMO - bool "Enable Auto Demo Mode" - default y - help - Automatically cycle through different expressions and eye movements. - - config EXPRESSIVE_EYES_DEMO_INTERVAL_MS - int "Demo Interval (ms)" - default 3000 - depends on EXPRESSIVE_EYES_AUTO_DEMO - help - Time in milliseconds between expression changes in demo mode. - - config EXPRESSIVE_EYES_ENABLE_PUPILS - bool "Enable Pupils" - default y - help - Draw pupils in the eyes. - - config EXPRESSIVE_EYES_PUPIL_SIZE - int "Pupil Size" - default 15 - depends on EXPRESSIVE_EYES_ENABLE_PUPILS - range 5 50 - help - Size of the pupils in pixels. - - config EXPRESSIVE_EYES_AUTO_BLINK - bool "Enable Auto Blink" - default y - help - Automatically blink the eyes at random intervals. - - config EXPRESSIVE_EYES_BLINK_INTERVAL_MS - int "Average Blink Interval (ms)" - default 4000 - depends on EXPRESSIVE_EYES_AUTO_BLINK - range 1000 10000 - help - Average time between blinks in milliseconds. - - config EXPRESSIVE_EYES_EYE_SPACING - int "Eye Spacing" - default 100 - range 20 300 - help - Distance between the centers of the two eyes in pixels. - - config EXPRESSIVE_EYES_EYE_WIDTH - int "Eye Width" - default 60 - range 20 200 - help - Width of each eye in pixels. - - config EXPRESSIVE_EYES_EYE_HEIGHT - int "Eye Height" - default 80 - range 20 200 - help - Height of each eye in pixels. - - config EXPRESSIVE_EYES_COLOR - hex "Eye Color (RGB565)" - default 0xFFFF - help - Color of the eyes in RGB565 format (default: white 0xFFFF). - endmenu diff --git a/components/expressive_eyes/example/sdkconfig.defaults b/components/expressive_eyes/example/sdkconfig.defaults index dc20163e6..13501caba 100644 --- a/components/expressive_eyes/example/sdkconfig.defaults +++ b/components/expressive_eyes/example/sdkconfig.defaults @@ -21,15 +21,3 @@ CONFIG_SPIRAM_SPEED_80M=y # Default board selection CONFIG_EXPRESSIVE_EYES_BOARD_ESP_BOX=y - -# Default settings -CONFIG_EXPRESSIVE_EYES_AUTO_DEMO=y -CONFIG_EXPRESSIVE_EYES_DEMO_INTERVAL_MS=3000 -CONFIG_EXPRESSIVE_EYES_ENABLE_PUPILS=y -CONFIG_EXPRESSIVE_EYES_PUPIL_SIZE=15 -CONFIG_EXPRESSIVE_EYES_AUTO_BLINK=y -CONFIG_EXPRESSIVE_EYES_BLINK_INTERVAL_MS=4000 -CONFIG_EXPRESSIVE_EYES_EYE_SPACING=100 -CONFIG_EXPRESSIVE_EYES_EYE_WIDTH=60 -CONFIG_EXPRESSIVE_EYES_EYE_HEIGHT=80 -CONFIG_EXPRESSIVE_EYES_COLOR=0xFFFF From d98b259525588fe2c42c05b95da542da439ca352 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 11:48:28 -0600 Subject: [PATCH 13/21] fix animation state tracking to improve blending between states; flesh out some more of the expressions --- .../example/main/expressive_eyes_example.cpp | 28 ++-- .../example/main/full_featured_drawer.hpp | 2 +- .../example/main/monochrome_blue_drawer.hpp | 2 +- .../include/expressive_eyes.hpp | 13 +- .../expressive_eyes/src/expressive_eyes.cpp | 151 ++++++++++++++---- 5 files changed, 146 insertions(+), 50 deletions(-) mode change 100755 => 100644 components/expressive_eyes/src/expressive_eyes.cpp diff --git a/components/expressive_eyes/example/main/expressive_eyes_example.cpp b/components/expressive_eyes/example/main/expressive_eyes_example.cpp index f3d9cdc01..61a86023d 100644 --- a/components/expressive_eyes/example/main/expressive_eyes_example.cpp +++ b/components/expressive_eyes/example/main/expressive_eyes_example.cpp @@ -116,7 +116,6 @@ extern "C" void app_main(void) { .eye_spacing = large_spacing, .eye_width = large_eye_width, .eye_height = large_eye_height, - .eye_color = 0xFFFF, .blink_duration = 0.12f, .blink_interval = 4.0f, .enable_auto_blink = true, @@ -149,19 +148,18 @@ extern "C" void app_main(void) { // Cycle through each expression preset to demonstrate different emotional states const espp::ExpressiveEyes::Expression expressions[] = { - espp::ExpressiveEyes::Expression::NEUTRAL, espp::ExpressiveEyes::Expression::HAPPY, - espp::ExpressiveEyes::Expression::SAD, espp::ExpressiveEyes::Expression::ANGRY, - espp::ExpressiveEyes::Expression::SURPRISED}; + espp::ExpressiveEyes::Expression::NEUTRAL, espp::ExpressiveEyes::Expression::HAPPY, + espp::ExpressiveEyes::Expression::SAD, espp::ExpressiveEyes::Expression::ANGRY, + espp::ExpressiveEyes::Expression::SURPRISED, espp::ExpressiveEyes::Expression::SLEEPY, + espp::ExpressiveEyes::Expression::BORED, espp::ExpressiveEyes::Expression::WINK_LEFT, + espp::ExpressiveEyes::Expression::WINK_RIGHT}; - auto last_time = std::chrono::steady_clock::now(); for (const auto &expr : expressions) { logger.info("Expression: {}", espp::ExpressiveEyes::expression_name(expr)); eyes.set_expression(expr); - for (int i = 0; i < 180; i++) { // 3 seconds at 60fps - auto now = std::chrono::steady_clock::now(); - float dt = std::chrono::duration(now - last_time).count(); - last_time = now; - eyes.update(dt); + auto start = std::chrono::steady_clock::now(); + while (std::chrono::duration(std::chrono::steady_clock::now() - start).count() < 3.0f) { + eyes.update(0.016f); // 16ms update at ~60fps std::this_thread::sleep_for(16ms); } } @@ -186,11 +184,9 @@ extern "C" void app_main(void) { for (const auto &dir : look_directions) { logger.info("Looking {}", dir.name); eyes.look_at(dir.x, dir.y); - for (int i = 0; i < 90; i++) { // 1.5 seconds at 60fps (faster) - auto now = std::chrono::steady_clock::now(); - float dt = std::chrono::duration(now - last_time).count(); - last_time = now; - eyes.update(dt); + auto start = std::chrono::steady_clock::now(); + while (std::chrono::duration(std::chrono::steady_clock::now() - start).count() < 1.5f) { + eyes.update(0.016f); // 16ms update at ~60fps std::this_thread::sleep_for(16ms); } } @@ -212,7 +208,7 @@ extern "C" void app_main(void) { float expression_timer = 0.0f; // Animation loop - runs indefinitely for continuous desk display - last_time = std::chrono::steady_clock::now(); + auto last_time = std::chrono::steady_clock::now(); while (true) { auto now = std::chrono::steady_clock::now(); float dt = std::chrono::duration(now - last_time).count(); diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp index 09597f008..a7b8a2eb2 100644 --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -144,7 +144,7 @@ class FullFeaturedDrawer : public EyeDrawer { // For left eye, positive angle tilts left side down (clockwise rotation) // For right eye, positive angle tilts right side down (counter-clockwise rotation) - float angle_rad = eye_state.expression.eyebrow.angle * M_PI / 180.0f; + float angle_rad = eye_state.expression.eyebrow.angle; // Already in radians if (!is_left) angle_rad = -angle_rad; // Mirror for right eye diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp index 53538e5ec..da47e472b 100644 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -110,7 +110,7 @@ class MonochromeBlueDrawer : public EyeDrawer { // For left eye, positive angle tilts left side down (clockwise rotation) // For right eye, positive angle tilts right side down (counter-clockwise rotation) - float angle_rad = eye_state.expression.eyebrow.angle * M_PI / 180.0f; + float angle_rad = eye_state.expression.eyebrow.angle; // Already in radians if (!is_left) angle_rad = -angle_rad; // Mirror for right eye diff --git a/components/expressive_eyes/include/expressive_eyes.hpp b/components/expressive_eyes/include/expressive_eyes.hpp index 06d0e156f..92ebed119 100644 --- a/components/expressive_eyes/include/expressive_eyes.hpp +++ b/components/expressive_eyes/include/expressive_eyes.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include #include @@ -43,7 +45,7 @@ class ExpressiveEyes : public BaseComponent { */ struct Eyebrow { bool enabled{false}; ///< Whether to draw eyebrows - float angle{0.0f}; ///< Eyebrow angle in radians + float angle{0.0f}; ///< Eyebrow angle in degrees float height{0.0f}; ///< Vertical offset relative to eye (-1.0 to 1.0) float thickness{0.1f}; ///< Eyebrow thickness relative to eye float width{1.0f}; ///< Eyebrow width relative to eye @@ -97,11 +99,12 @@ class ExpressiveEyes : public BaseComponent { */ enum class Expression { NEUTRAL, ///< Normal open eyes - HAPPY, ///< Squinted happy eyes with raised eyebrows + HAPPY, ///< Squinted happy eyes with raised cheeks SAD, ///< Droopy sad eyes with angled eyebrows ANGRY, ///< Angled angry eyes with furrowed eyebrows SURPRISED, ///< Wide open eyes with raised eyebrows - SLEEPY, ///< Half-closed eyes + SLEEPY, ///< Droopy half-closed eyes with low eyebrows + BORED, ///< Half-closed eyes with neutral expression WINK_LEFT, ///< Left eye closed WINK_RIGHT, ///< Right eye closed }; @@ -125,6 +128,8 @@ class ExpressiveEyes : public BaseComponent { return "Surprised"; case Expression::SLEEPY: return "Sleepy"; + case Expression::BORED: + return "Bored"; case Expression::WINK_LEFT: return "Wink Left"; case Expression::WINK_RIGHT: @@ -143,7 +148,6 @@ class ExpressiveEyes : public BaseComponent { int eye_spacing{100}; ///< Distance between eye centers int eye_width{60}; ///< Base eye width int eye_height{80}; ///< Base eye height - uint16_t eye_color{0xFFFF}; ///< Eye color (white) float blink_duration{0.12f}; ///< Blink duration in seconds float blink_interval{4.0f}; ///< Average time between blinks bool enable_auto_blink{true}; ///< Automatic random blinking @@ -208,6 +212,7 @@ class ExpressiveEyes : public BaseComponent { float clamp(float v, float min, float max) { return std::max(min, std::min(max, v)); } Config config_; + Expression source_expression_{Expression::NEUTRAL}; ///< Source expression for blending Expression current_expression_{Expression::NEUTRAL}; Expression target_expression_{Expression::NEUTRAL}; ExpressionState current_state_; diff --git a/components/expressive_eyes/src/expressive_eyes.cpp b/components/expressive_eyes/src/expressive_eyes.cpp old mode 100755 new mode 100644 index c7708eb26..e6d8b4b0b --- a/components/expressive_eyes/src/expressive_eyes.cpp +++ b/components/expressive_eyes/src/expressive_eyes.cpp @@ -1,4 +1,6 @@ #include "expressive_eyes.hpp" + +#include #include using namespace espp; @@ -111,20 +113,35 @@ void ExpressiveEyes::update_pupils(float dt) { void ExpressiveEyes::update_expression(float dt) { if (current_expression_ != target_expression_) { - // Start blend - current_expression_ = target_expression_; + // Start new blend - save current blended state as source + source_expression_ = current_expression_; target_state_ = get_preset_expression(target_expression_); expression_blend_ = 0.0f; + current_expression_ = target_expression_; // Update tracking immediately } // Blend towards target if (expression_blend_ < 1.0f) { - expression_blend_ += dt * 3.0f; // Blend over ~0.33 seconds (slower, smoother) - if (expression_blend_ > 1.0f) + expression_blend_ += dt * 8.0f; // Blend over ~0.125 seconds (fast, smooth transitions) + if (expression_blend_ > 1.0f) { expression_blend_ = 1.0f; + } - ExpressionState from = get_preset_expression(current_expression_); + ExpressionState from = get_preset_expression(source_expression_); + // Save current pupil offset before blending + float current_pupil_x = current_state_.pupil.x; + float current_pupil_y = current_state_.pupil.y; blend_expression_states(current_state_, from, target_state_, expression_blend_); + // Restore pupil offset (controlled by update_pupils, not expression blend) + current_state_.pupil.x = current_pupil_x; + current_state_.pupil.y = current_pupil_y; + } else { + // Blend complete, use target state but preserve pupil offset + float current_pupil_x = current_state_.pupil.x; + float current_pupil_y = current_state_.pupil.y; + current_state_ = target_state_; + current_state_.pupil.x = current_pupil_x; + current_state_.pupil.y = current_pupil_y; } } @@ -136,18 +153,56 @@ void ExpressiveEyes::blend_expression_states(ExpressionState &result, const Expr result.top_curve = lerp(from.top_curve, to.top_curve, t); result.bottom_curve = lerp(from.bottom_curve, to.bottom_curve, t); result.eye_offset_y = lerp(from.eye_offset_y, to.eye_offset_y, t); - result.cheek_offset_y = lerp(from.cheek_offset_y, to.cheek_offset_y, t); - // Blend eyebrow - result.eyebrow.enabled = from.eyebrow.enabled || to.eyebrow.enabled; - result.eyebrow.angle = lerp(from.eyebrow.angle, to.eyebrow.angle, t); - result.eyebrow.height = lerp(from.eyebrow.height, to.eyebrow.height, t); - result.eyebrow.thickness = lerp(from.eyebrow.thickness, to.eyebrow.thickness, t); + // Eyebrow blending - animate sliding in/out + if (from.eyebrow.enabled && to.eyebrow.enabled) { + // Both enabled - blend normally + result.eyebrow.enabled = true; + result.eyebrow.angle = lerp(from.eyebrow.angle, to.eyebrow.angle, t); + result.eyebrow.height = lerp(from.eyebrow.height, to.eyebrow.height, t); + result.eyebrow.thickness = lerp(from.eyebrow.thickness, to.eyebrow.thickness, t); + result.eyebrow.width = lerp(from.eyebrow.width, to.eyebrow.width, t); + } else if (!from.eyebrow.enabled && to.eyebrow.enabled) { + // Fading in - slide down from above + result.eyebrow.enabled = true; + result.eyebrow.angle = to.eyebrow.angle; + result.eyebrow.height = lerp(-1.2f, to.eyebrow.height, t); // Start above screen + result.eyebrow.thickness = to.eyebrow.thickness; + result.eyebrow.width = to.eyebrow.width; + } else if (from.eyebrow.enabled && !to.eyebrow.enabled) { + // Fading out - slide up above screen + result.eyebrow.enabled = (t < 1.0f); + result.eyebrow.angle = from.eyebrow.angle; + result.eyebrow.height = lerp(from.eyebrow.height, -1.2f, t); // Slide up + result.eyebrow.thickness = from.eyebrow.thickness; + result.eyebrow.width = from.eyebrow.width; + } else { + // Both disabled + result.eyebrow.enabled = false; + } - // Cheek enabled is binary, use the target state - result.cheek.enabled = t < 0.5f ? from.cheek.enabled : to.cheek.enabled; + // Cheek blending - animate sliding in/out + if (from.cheek.enabled && to.cheek.enabled) { + // Both enabled - blend normally + result.cheek.enabled = true; + result.cheek.size = lerp(from.cheek.size, to.cheek.size, t); + result.cheek_offset_y = lerp(from.cheek_offset_y, to.cheek_offset_y, t); + } else if (!from.cheek.enabled && to.cheek.enabled) { + // Fading in - slide up from below + result.cheek.enabled = true; + result.cheek.size = to.cheek.size; + result.cheek_offset_y = lerp(1.2f, to.cheek_offset_y, t); // Start below screen + } else if (from.cheek.enabled && !to.cheek.enabled) { + // Fading out - slide down below screen + result.cheek.enabled = (t < 1.0f); + result.cheek.size = from.cheek.size; + result.cheek_offset_y = lerp(from.cheek_offset_y, 1.2f, t); // Slide down + } else { + // Both disabled + result.cheek.enabled = false; + } - // Keep pupil settings from target + // Keep pupil settings from target but use current look position result.pupil = to.pupil; result.pupil.x = current_look_x_; result.pupil.y = current_look_y_; @@ -189,22 +244,36 @@ void ExpressiveEyes::draw_eyes() { // Compute final eye dimensions int eye_width = static_cast(config_.eye_width * current_state_.eye_width_scale); - int eye_height = static_cast(config_.eye_height * current_state_.eye_height_scale * - (1.0f - blink_amount)); + int base_eye_height = static_cast(config_.eye_height * current_state_.eye_height_scale); + + // Handle wink expressions - apply different blink amounts to each eye + float left_blink = blink_amount; + float right_blink = blink_amount; + + if (current_expression_ == Expression::WINK_LEFT) { + left_blink = 0.9f; // Almost closed + right_blink = 0.0f; // Open + } else if (current_expression_ == Expression::WINK_RIGHT) { + left_blink = 0.0f; // Open + right_blink = 0.9f; // Almost closed + } + + int left_eye_height = static_cast(base_eye_height * (1.0f - left_blink)); + int right_eye_height = static_cast(base_eye_height * (1.0f - right_blink)); // Create state for each eye EyeState left_eye; left_eye.x = left_x; left_eye.y = eye_y; left_eye.width = eye_width; - left_eye.height = eye_height; + left_eye.height = left_eye_height; left_eye.expression = current_state_; EyeState right_eye; right_eye.x = right_x; right_eye.y = eye_y; right_eye.width = eye_width; - right_eye.height = eye_height; + right_eye.height = right_eye_height; right_eye.expression = current_state_; right_eye.expression.eye_rotation = -current_state_.eye_rotation; // Mirror rotation @@ -254,10 +323,10 @@ ExpressiveEyes::ExpressionState ExpressiveEyes::get_preset_expression(Expression state.top_curve = 0.5f; state.bottom_curve = 0.5f; state.eye_offset_y = -0.08f; // Move eyes up slightly on face - state.cheek_offset_y = 0.05f; // Move cheeks lower (positive is down) + state.cheek_offset_y = 0.18f; // Move cheeks much lower (positive is down) // No eyebrows for happy (hidden by default) state.cheek.enabled = true; // Shape bottom with cheeks - state.cheek.size = 0.6f; // Wider cheeks + state.cheek.size = 0.8f; // Wider, more prominent cheeks break; case Expression::SAD: @@ -267,14 +336,14 @@ ExpressiveEyes::ExpressionState ExpressiveEyes::get_preset_expression(Expression state.top_curve = 0.3f; // Flatter top state.bottom_curve = 0.7f; // Rounder bottom state.eyebrow.enabled = true; - state.eyebrow.angle = -20.0f; // Angled UP toward center (sad brows) - in degrees + state.eyebrow.angle = -0.436f; // Angled UP toward center (sad brows) - ~25 degrees in radians state.eyebrow.height = -0.35f; - state.eyebrow.thickness = 0.08f; + state.eyebrow.thickness = 0.1f; state.eyebrow.width = 1.0f; state.eyebrow.color = 0x0000; - // Don't override pupil size state.cheek.enabled = true; // Shape bottom with cheeks - state.cheek.size = 0.4f; + state.cheek.size = 0.6f; + state.cheek_offset_y = 0.15f; // Position below eye break; case Expression::ANGRY: @@ -284,8 +353,9 @@ ExpressiveEyes::ExpressionState ExpressiveEyes::get_preset_expression(Expression state.top_curve = 0.2f; // Angular top state.bottom_curve = 0.3f; state.eyebrow.enabled = true; - state.eyebrow.angle = 25.0f; // Angled DOWN toward center (angry furrowed brows) - in degrees - state.eyebrow.height = -0.2f; // Close to eyes + state.eyebrow.angle = + 0.524f; // Angled DOWN toward center (angry furrowed brows) - ~30 degrees in radians + state.eyebrow.height = -0.15f; // Close to eyes state.eyebrow.thickness = 0.12f; // Thicker state.eyebrow.width = 0.9f; state.eyebrow.color = 0x0000; @@ -304,15 +374,40 @@ ExpressiveEyes::ExpressionState ExpressiveEyes::get_preset_expression(Expression case Expression::SLEEPY: state.eye_width_scale = 1.0f; - state.eye_height_scale = 0.25f; // Very closed + state.eye_height_scale = 0.35f; // Eyes mostly closed, droopy + state.eye_rotation = -0.1f; // Slight downward droop + state.top_curve = 0.3f; + state.bottom_curve = 0.3f; + state.pupil.enabled = true; // Keep pupils visible + state.eyebrow.enabled = true; + state.eyebrow.angle = 0.0f; // Straight across + state.eyebrow.height = -0.15f; // Lower, almost touching eyes + state.eyebrow.thickness = 0.18f; // Very thick, heavy looking + state.eyebrow.width = 1.0f; + state.eyebrow.color = 0x0000; + break; + + case Expression::BORED: + state.eye_width_scale = 1.0f; + state.eye_height_scale = 0.5f; // Half-closed state.eye_rotation = 0.0f; state.top_curve = 0.3f; state.bottom_curve = 0.3f; - state.pupil.enabled = false; // Pupils hidden + state.pupil.enabled = true; + // No eyebrows for bored - just half-closed neutral eyes break; case Expression::WINK_LEFT: + // Base on neutral, blink applied per-eye in draw_eyes() + state.eye_width_scale = 1.0f; + state.eye_height_scale = 1.0f; + state.eye_rotation = 0.0f; + state.top_curve = 0.5f; + state.bottom_curve = 0.5f; + break; + case Expression::WINK_RIGHT: + // Base on neutral, blink applied per-eye in draw_eyes() state.eye_width_scale = 1.0f; state.eye_height_scale = 1.0f; state.eye_rotation = 0.0f; From e9e09db7a9766ef7b52c8e245ede0f952ae01445 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 11:54:48 -0600 Subject: [PATCH 14/21] doc: update --- components/expressive_eyes/README.md | 3 ++- components/expressive_eyes/idf_component.yml | 2 ++ doc/en/display/expressive_eyes.rst | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/expressive_eyes/README.md b/components/expressive_eyes/README.md index 11ec62cc7..d56b9edc1 100755 --- a/components/expressive_eyes/README.md +++ b/components/expressive_eyes/README.md @@ -11,11 +11,12 @@ custom renderers for different display types and visual styles. ## Features -- Multiple expressions (happy, sad, angry, surprised, neutral, sleepy, wink) +- Multiple expressions (happy, sad, angry, surprised, neutral, sleepy, bored, wink_left, wink_right) - Smooth eye movement with look_at positioning - Automatic blinking with configurable intervals - Optional pupils with physics-based movement - Eyebrows and cheeks for enhanced expressions +- Smooth expression transitions with blending - Customizable colors and sizes - Frame-based animation system diff --git a/components/expressive_eyes/idf_component.yml b/components/expressive_eyes/idf_component.yml index c9f803597..93f9e1434 100644 --- a/components/expressive_eyes/idf_component.yml +++ b/components/expressive_eyes/idf_component.yml @@ -17,3 +17,5 @@ dependencies: idf: version: '>=5.0' espp/base_component: '>=1.0' +examples: + - path: example diff --git a/doc/en/display/expressive_eyes.rst b/doc/en/display/expressive_eyes.rst index 80d31be56..796028a8e 100755 --- a/doc/en/display/expressive_eyes.rst +++ b/doc/en/display/expressive_eyes.rst @@ -12,11 +12,12 @@ minimalist monochrome designs. Features include: -- Multiple expressions (happy, sad, angry, surprised, neutral) +- Multiple expressions (happy, sad, angry, surprised, neutral, sleepy, bored, wink_left, wink_right) - Smooth eye movement with look_at positioning - Automatic blinking with configurable intervals - Optional pupils with physics-based movement - Eyebrows and cheeks for enhanced expressions +- Smooth expression transitions with blending - Customizable colors and sizes - Frame-based animation system From d82c1c0b3cfce571382c31e2a35b3b9927767ae4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 11:55:26 -0600 Subject: [PATCH 15/21] fix sa --- .../expressive_eyes/example/main/full_featured_drawer.hpp | 2 +- .../expressive_eyes/example/main/monochrome_blue_drawer.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp index a7b8a2eb2..65b7734b1 100644 --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -59,7 +59,7 @@ class FullFeaturedDrawer : public EyeDrawer { }; } - virtual void cleanup() override { + void cleanup() override { // No dynamic resources to free } diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp index da47e472b..5120eb603 100644 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -57,7 +57,7 @@ class MonochromeBlueDrawer : public EyeDrawer { }; } - virtual void cleanup() override { + void cleanup() override { // No dynamic resources to free } From 66a095c5bddddfd8d2fcbe1739f88f251461f6d6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 16:07:54 -0600 Subject: [PATCH 16/21] fix sa --- components/expressive_eyes/example/main/eye_drawer.hpp | 8 -------- .../expressive_eyes/example/main/full_featured_drawer.hpp | 8 +++----- .../example/main/monochrome_blue_drawer.hpp | 8 +++----- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/components/expressive_eyes/example/main/eye_drawer.hpp b/components/expressive_eyes/example/main/eye_drawer.hpp index 46782664b..2ca411fb1 100644 --- a/components/expressive_eyes/example/main/eye_drawer.hpp +++ b/components/expressive_eyes/example/main/eye_drawer.hpp @@ -34,14 +34,6 @@ struct EyeDrawer { * @return The callback function to be used with ExpressiveEyes */ virtual DrawCallback get_draw_callback() = 0; - - /** - * @brief Clean up resources - * - * Called when the drawer is being destroyed. Implementations should - * free any allocated resources. - */ - virtual void cleanup() = 0; }; } // namespace eye_drawer diff --git a/components/expressive_eyes/example/main/full_featured_drawer.hpp b/components/expressive_eyes/example/main/full_featured_drawer.hpp index 65b7734b1..25cf6b6df 100644 --- a/components/expressive_eyes/example/main/full_featured_drawer.hpp +++ b/components/expressive_eyes/example/main/full_featured_drawer.hpp @@ -40,7 +40,9 @@ class FullFeaturedDrawer : public EyeDrawer { pupil_size_ = static_cast(std::min(eye_base_width_, original_eye_height_) * 0.3f); } - virtual ~FullFeaturedDrawer() override { cleanup(); } + virtual ~FullFeaturedDrawer() override { + // nothing to clean up + } virtual DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, @@ -59,10 +61,6 @@ class FullFeaturedDrawer : public EyeDrawer { }; } - void cleanup() override { - // No dynamic resources to free - } - private: /** * @brief Draw a filled ellipse using LVGL layer API diff --git a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp index 5120eb603..a867c735c 100644 --- a/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp +++ b/components/expressive_eyes/example/main/monochrome_blue_drawer.hpp @@ -41,7 +41,9 @@ class MonochromeBlueDrawer : public EyeDrawer { eye_base_width_ = screen_width_ * 0.35f; } - virtual ~MonochromeBlueDrawer() override { cleanup(); } + virtual ~MonochromeBlueDrawer() override { + // nothing to clean up + } virtual DrawCallback get_draw_callback() override { return [this](const espp::ExpressiveEyes::EyeState &left, @@ -57,10 +59,6 @@ class MonochromeBlueDrawer : public EyeDrawer { }; } - void cleanup() override { - // No dynamic resources to free - } - private: /** * @brief Draw a filled ellipse using LVGL layer API From 70cdcf24df6eaa14ccd351e571ac4566f2e8e638 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 16:40:38 -0600 Subject: [PATCH 17/21] readme: update --- components/expressive_eyes/README.md | 30 +++++++---- components/expressive_eyes/example/README.md | 55 +++++++++++++------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/components/expressive_eyes/README.md b/components/expressive_eyes/README.md index d56b9edc1..9e872920e 100755 --- a/components/expressive_eyes/README.md +++ b/components/expressive_eyes/README.md @@ -9,6 +9,8 @@ display various emotions with smooth animations. The component uses a callback-based drawing system, allowing you to implement custom renderers for different display types and visual styles. +https://github.com/user-attachments/assets/966f5e63-aeb8-4fc3-b915-c4936fee7a1f + ## Features - Multiple expressions (happy, sad, angry, surprised, neutral, sleepy, bored, wink_left, wink_right) @@ -22,23 +24,31 @@ custom renderers for different display types and visual styles. - - + + + + + + + + + + - - + + - - + + + + + +
happy imagesad imagehappy imagesad image
angry imagesurprised image
sleepy imagebored image
angry imagesurprised imagewink left imagewink right image
looking imagelooking imagelooking left imagelooking right image
looking up imagelooking down image
-Videos: - -https://github.com/user-attachments/assets/42f5db22-1b2b-4a66-945f-542e57df55bf - ## Example The [example](./example) demonstrates the expressive eyes component with two diff --git a/components/expressive_eyes/example/README.md b/components/expressive_eyes/example/README.md index a78f10a68..bce2f13fc 100755 --- a/components/expressive_eyes/example/README.md +++ b/components/expressive_eyes/example/README.md @@ -6,22 +6,34 @@ continuous random demo mode perfect for a desk display. - - + + - - + + - - + + + + + + + + + + + + + +
happy imagesad imagehappy imagesad image
angry imagesurprised imageangry imagesurprised image
looking imagelooking imagesleepy imagebored image
wink left imagewink right image
looking left imagelooking right image
looking up imagelooking down image
Videos: -https://github.com/user-attachments/assets/42f5db22-1b2b-4a66-945f-542e57df55bf +https://github.com/user-attachments/assets/966f5e63-aeb8-4fc3-b915-c4936fee7a1f ## How to use example @@ -115,24 +127,31 @@ See [README_DRAWERS.md](./main/README_DRAWERS.md) for details on creating custom - - + + + + + + - - + + - - + + + + + + + + + +
happy imagesad imagehappy imagesad image
angry imagesurprised image
angry imagesurprised imagesleepy imagebored image
looking imagelooking imagewink left imagewink right image
looking left imagelooking right image
looking up imagelooking down image
-Videos: - -https://github.com/user-attachments/assets/42f5db22-1b2b-4a66-945f-542e57df55bf - - image ```console From b51b72d3b0f207dd9eb2f4f1271d883f2f475578 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 16:43:31 -0600 Subject: [PATCH 18/21] remove unnecessary drawer readme --- .../example/main/README_DRAWERS.md | 104 ------------------ 1 file changed, 104 deletions(-) delete mode 100755 components/expressive_eyes/example/main/README_DRAWERS.md diff --git a/components/expressive_eyes/example/main/README_DRAWERS.md b/components/expressive_eyes/example/main/README_DRAWERS.md deleted file mode 100755 index b18a1583d..000000000 --- a/components/expressive_eyes/example/main/README_DRAWERS.md +++ /dev/null @@ -1,104 +0,0 @@ -# Eye Drawer Implementations - -This directory contains different implementations for drawing expressive eyes. The drawing logic has been separated into modular components to demonstrate different visual styles and allow easy customization. - -## Architecture - -The drawer system uses a base interface (`EyeDrawer`) with concrete implementations: - -- **`eye_drawer.hpp`**: Base interface that all drawers must implement -- **`full_featured_drawer.hpp`**: Traditional realistic eyes with white background and black pupils -- **`monochrome_blue_drawer.hpp`**: Electric blue eyes on black background (no pupils) - -## Available Drawers - -### Full Featured Drawer - -Draws realistic-looking eyes with: -- White eye background -- Black pupils with position tracking -- Eyebrows with rotation support (using lv_draw_line for smooth rendering) -- Cheeks for emotional expressions -- Uses background color from LVGL theme - -**Best for**: Traditional robot/character displays, realistic eye expressions - -### Monochrome Blue Drawer - -Draws minimalist electric blue eyes with: -- Electric blue (#00BFFF) eye shapes -- Black background -- No pupils (solid colored eyes) -- Black eyebrows and cheeks (using lv_draw_line with rounded caps) -- Clean, modern aesthetic - -**Best for**: Sci-fi/futuristic interfaces, minimal power displays, WS-S3-Touch boards - -## Configuration - -The drawing method is selected via menuconfig: -``` -Expressive Eyes Example Configuration > Select Drawing Method -``` - -Options: -- **Full Featured** (default for most boards) -- **Monochrome Blue** (default for WS-S3-Touch) - -## Adding New Drawers - -To create a new drawer implementation: - -1. Create a new header file (e.g., `my_custom_drawer.hpp`) -2. Inherit from `eye_drawer::EyeDrawer` -3. Implement required methods: - - `get_draw_callback()`: Return the drawing function - - `cleanup()`: Clean up resources if needed -4. Add config option to `Kconfig.projbuild` -5. Add conditional compilation to `expressive_eyes_example.cpp` - -### Example Template - -```cpp -#pragma once -#include "eye_drawer.hpp" - -namespace eye_drawer { - -class MyCustomDrawer : public EyeDrawer { -public: - struct Config { - int screen_width; - int screen_height; - lv_obj_t *canvas; - lv_color_t *canvas_buffer; - std::recursive_mutex &lvgl_mutex; - }; - - explicit MyCustomDrawer(const Config &config) { /* ... */ } - - std::function - get_draw_callback() override { - return [this](const espp::ExpressiveEyes::EyeState &left, - const espp::ExpressiveEyes::EyeState &right) { - // Your drawing code here - }; - } - - void cleanup() override { /* ... */ } -}; - -} // namespace eye_drawer -``` - -## Drawing Tips - -- Use `lv_canvas_init_layer()` and `lv_canvas_finish_layer()` for each drawing operation -- Access eye state through `EyeState` structure (position, size, expression) -- Lock `lvgl_mutex` before any LVGL operations -- Use `eye_state.height / original_eye_height` to detect blink state -- Mirror eyebrow angles for left vs right eyes -- Use `screen_width` and `screen_height` for relative positioning -- For eyebrows, use `lv_draw_line` with width parameter instead of triangles for better performance and no seam artifacts -- Set `round_start` and `round_end` to 1 for smooth rounded line caps From a15bc5120d5164f67578ff0e5b334265979f5bbf Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 16:47:26 -0600 Subject: [PATCH 19/21] replace with libfmt formatting --- .../example/main/expressive_eyes_example.cpp | 2 +- .../include/expressive_eyes.hpp | 72 +++++++++++-------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/components/expressive_eyes/example/main/expressive_eyes_example.cpp b/components/expressive_eyes/example/main/expressive_eyes_example.cpp index 61a86023d..763bee1cc 100644 --- a/components/expressive_eyes/example/main/expressive_eyes_example.cpp +++ b/components/expressive_eyes/example/main/expressive_eyes_example.cpp @@ -155,7 +155,7 @@ extern "C" void app_main(void) { espp::ExpressiveEyes::Expression::WINK_RIGHT}; for (const auto &expr : expressions) { - logger.info("Expression: {}", espp::ExpressiveEyes::expression_name(expr)); + logger.info("Expression: {}", expr); eyes.set_expression(expr); auto start = std::chrono::steady_clock::now(); while (std::chrono::duration(std::chrono::steady_clock::now() - start).count() < 3.0f) { diff --git a/components/expressive_eyes/include/expressive_eyes.hpp b/components/expressive_eyes/include/expressive_eyes.hpp index 92ebed119..cdb36edfc 100644 --- a/components/expressive_eyes/include/expressive_eyes.hpp +++ b/components/expressive_eyes/include/expressive_eyes.hpp @@ -109,36 +109,6 @@ class ExpressiveEyes : public BaseComponent { WINK_RIGHT, ///< Right eye closed }; - /** - * @brief Get string name for expression - * @param expr Expression enum - * @return String name of expression - */ - static constexpr const char *expression_name(Expression expr) { - switch (expr) { - case Expression::NEUTRAL: - return "Neutral"; - case Expression::HAPPY: - return "Happy"; - case Expression::SAD: - return "Sad"; - case Expression::ANGRY: - return "Angry"; - case Expression::SURPRISED: - return "Surprised"; - case Expression::SLEEPY: - return "Sleepy"; - case Expression::BORED: - return "Bored"; - case Expression::WINK_LEFT: - return "Wink Left"; - case Expression::WINK_RIGHT: - return "Wink Right"; - default: - return "Unknown"; - } - } - /** * @brief Configuration for expressive eyes */ @@ -239,3 +209,45 @@ class ExpressiveEyes : public BaseComponent { float redraw_timer_{0.0f}; }; } // namespace espp + +// for libfmt formatting of ExpressiveEyes::Expression +template <> struct fmt::formatter { + constexpr auto parse(format_parse_context &ctx) { return ctx.begin(); } + template + auto format(const espp::ExpressiveEyes::Expression &expr, FormatContext &ctx) const { + std::string name; + switch (expr) { + case espp::ExpressiveEyes::Expression::NEUTRAL: + name = "NEUTRAL"; + break; + case espp::ExpressiveEyes::Expression::HAPPY: + name = "HAPPY"; + break; + case espp::ExpressiveEyes::Expression::SAD: + name = "SAD"; + break; + case espp::ExpressiveEyes::Expression::ANGRY: + name = "ANGRY"; + break; + case espp::ExpressiveEyes::Expression::SURPRISED: + name = "SURPRISED"; + break; + case espp::ExpressiveEyes::Expression::SLEEPY: + name = "SLEEPY"; + break; + case espp::ExpressiveEyes::Expression::BORED: + name = "BORED"; + break; + case espp::ExpressiveEyes::Expression::WINK_LEFT: + name = "WINK_LEFT"; + break; + case espp::ExpressiveEyes::Expression::WINK_RIGHT: + name = "WINK_RIGHT"; + break; + default: + name = "UNKNOWN"; + break; + } + return fmt::format_to(ctx.out(), "{}", name); + } +}; From a04abeccd851b828e18f7ed88ccdbca2bc55e154 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 16:49:44 -0600 Subject: [PATCH 20/21] cleanup --- components/expressive_eyes/include/expressive_eyes.hpp | 1 - components/expressive_eyes/src/expressive_eyes.cpp | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/components/expressive_eyes/include/expressive_eyes.hpp b/components/expressive_eyes/include/expressive_eyes.hpp index cdb36edfc..c5eda8977 100644 --- a/components/expressive_eyes/include/expressive_eyes.hpp +++ b/components/expressive_eyes/include/expressive_eyes.hpp @@ -179,7 +179,6 @@ class ExpressiveEyes : public BaseComponent { const ExpressionState &to, float t); float lerp(float a, float b, float t) { return a + (b - a) * t; } - float clamp(float v, float min, float max) { return std::max(min, std::min(max, v)); } Config config_; Expression source_expression_{Expression::NEUTRAL}; ///< Source expression for blending diff --git a/components/expressive_eyes/src/expressive_eyes.cpp b/components/expressive_eyes/src/expressive_eyes.cpp index e6d8b4b0b..eaeefc8a4 100644 --- a/components/expressive_eyes/src/expressive_eyes.cpp +++ b/components/expressive_eyes/src/expressive_eyes.cpp @@ -97,8 +97,8 @@ void ExpressiveEyes::update_pupils(float dt) { current_look_y_ += pupil_velocity_y_ * dt; // Clamp to valid range - current_look_x_ = clamp(current_look_x_, -1.0f, 1.0f); - current_look_y_ = clamp(current_look_y_, -1.0f, 1.0f); + current_look_x_ = std::clamp(current_look_x_, -1.0f, 1.0f); + current_look_y_ = std::clamp(current_look_y_, -1.0f, 1.0f); } } else { // Direct movement @@ -282,8 +282,8 @@ void ExpressiveEyes::draw_eyes() { } void ExpressiveEyes::look_at(float x, float y) { - target_look_x_ = clamp(x, -1.0f, 1.0f); - target_look_y_ = clamp(y, -1.0f, 1.0f); + target_look_x_ = std::clamp(x, -1.0f, 1.0f); + target_look_y_ = std::clamp(y, -1.0f, 1.0f); } void ExpressiveEyes::set_expression(Expression expr) { From 7e0f404699220cd8250914fe26a29ed3ea52427c Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 16:55:19 -0600 Subject: [PATCH 21/21] ensure wink states are blended like the rest for smooth transitions --- .../expressive_eyes/src/expressive_eyes.cpp | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/components/expressive_eyes/src/expressive_eyes.cpp b/components/expressive_eyes/src/expressive_eyes.cpp index eaeefc8a4..536be08e0 100644 --- a/components/expressive_eyes/src/expressive_eyes.cpp +++ b/components/expressive_eyes/src/expressive_eyes.cpp @@ -246,16 +246,36 @@ void ExpressiveEyes::draw_eyes() { int eye_width = static_cast(config_.eye_width * current_state_.eye_width_scale); int base_eye_height = static_cast(config_.eye_height * current_state_.eye_height_scale); - // Handle wink expressions - apply different blink amounts to each eye - float left_blink = blink_amount; - float right_blink = blink_amount; - - if (current_expression_ == Expression::WINK_LEFT) { - left_blink = 0.9f; // Almost closed - right_blink = 0.0f; // Open - } else if (current_expression_ == Expression::WINK_RIGHT) { - left_blink = 0.0f; // Open - right_blink = 0.9f; // Almost closed + // Handle wink expressions - blend between wink states + float target_left_blink = blink_amount; + float target_right_blink = blink_amount; + + if (target_expression_ == Expression::WINK_LEFT) { + target_left_blink = 0.9f; // Almost closed + target_right_blink = 0.0f; // Open + } else if (target_expression_ == Expression::WINK_RIGHT) { + target_left_blink = 0.0f; // Open + target_right_blink = 0.9f; // Almost closed + } + + // If blending between expressions, lerp the wink amounts + float left_blink = target_left_blink; + float right_blink = target_right_blink; + + if (expression_blend_ < 1.0f) { + float source_left_blink = blink_amount; + float source_right_blink = blink_amount; + + if (source_expression_ == Expression::WINK_LEFT) { + source_left_blink = 0.9f; + source_right_blink = 0.0f; + } else if (source_expression_ == Expression::WINK_RIGHT) { + source_left_blink = 0.0f; + source_right_blink = 0.9f; + } + + left_blink = lerp(source_left_blink, target_left_blink, expression_blend_); + right_blink = lerp(source_right_blink, target_right_blink, expression_blend_); } int left_eye_height = static_cast(base_eye_height * (1.0f - left_blink));