From 9e52aab213ba8552a482134c0071242499298b55 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 19 Jan 2026 23:01:48 -0600 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 }