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, 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

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.

API Reference

Header File

Classes

class ExpressiveEyes : public espp::BaseComponent

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

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);

  // 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);

  // Create the drawer based on menuconfig selection
  std::unique_ptr<eye_drawer::EyeDrawer> drawer;

#if CONFIG_EXPRESSIVE_EYES_FULL_FEATURED
  logger.info("Using Full Featured drawer");
  drawer = std::make_unique<eye_drawer::FullFeaturedDrawer>(
      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>(
      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
  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,
                                      .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<std::recursive_mutex> lock(lvgl_mutex);
                          lv_task_handler();
                        }
                        std::unique_lock<std::mutex> 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...");

  // 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::SLEEPY,
      espp::ExpressiveEyes::Expression::BORED,     espp::ExpressiveEyes::Expression::WINK_LEFT,
      espp::ExpressiveEyes::Expression::WINK_RIGHT};

  for (const auto &expr : expressions) {
    logger.info("Expression: {}", expr);
    eyes.set_expression(expr);
    auto start = std::chrono::steady_clock::now();
    while (std::chrono::duration<float>(std::chrono::steady_clock::now() - start).count() < 3.0f) {
      eyes.update(0.016f); // 16ms update at ~60fps
      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);

  // Demonstrate directional looking in the 4 cardinal directions plus center
  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);
    auto start = std::chrono::steady_clock::now();
    while (std::chrono::duration<float>(std::chrono::steady_clock::now() - start).count() < 1.5f) {
      eyes.update(0.016f); // 16ms update at ~60fps
      std::this_thread::sleep_for(16ms);
    }
  }

  // Random demo mode - continuously looks around and changes expressions
  logger.info("Starting random demo mode - will run continuously");

  // Seed random number generator with current time
  srand(time(nullptr));

  // Reset to neutral expression and center gaze
  eyes.set_expression(espp::ExpressiveEyes::Expression::NEUTRAL);
  eyes.look_at(0.0f, 0.0f);

  // 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 - runs indefinitely for continuous desk display
  auto last_time = std::chrono::steady_clock::now();
  while (true) {
    auto now = std::chrono::steady_clock::now();
    float dt = std::chrono::duration<float>(now - last_time).count();
    last_time = now;

    // Update timers
    look_timer += dt;
    expression_timer += dt;

    // 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
      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 for natural behavior)
    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);

    std::this_thread::sleep_for(16ms); // ~60 FPS
  }

Public Types

enum class Expression

Eye expression presets.

Values:

enumerator NEUTRAL

Normal open eyes.

enumerator HAPPY

Squinted happy eyes with raised cheeks.

enumerator SAD

Droopy sad eyes with angled eyebrows.

enumerator ANGRY

Angled angry eyes with furrowed eyebrows.

enumerator SURPRISED

Wide open eyes with raised eyebrows.

enumerator SLEEPY

Droopy half-closed eyes with low eyebrows.

enumerator BORED

Half-closed eyes with neutral expression.

enumerator WINK_LEFT

Left eye closed.

enumerator WINK_RIGHT

Right eye closed.

typedef std::function<void(const EyeState &left_eye, const EyeState &right_eye)> draw_callback

Draw callback function.

Param left_eye:

Left eye render data

Param right_eye:

Right eye render data

Public Functions

explicit ExpressiveEyes(const Config &config)

Construct expressive eyes.

Parameters:

config – Configuration structure

void update(float dt)

Update animation (call this every frame)

Parameters:

dt – Delta time since last update in seconds

void look_at(float x, float y)

Set target look direction.

Parameters:
  • x – Horizontal look direction (-1.0 to 1.0, 0=center)

  • y – Vertical look direction (-1.0 to 1.0, 0=center)

void set_expression(Expression expr)

Set expression.

Parameters:

expr – Expression to display

void blink()

Trigger a blink.

inline Expression get_expression() const

Get current expression.

Returns:

Current expression

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns:

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters:

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters:

rate_limit – The rate limit to use for the logger

Public Static Functions

static ExpressionState get_preset_expression(Expression expr)

Get preset expression state.

Parameters:

expr – Expression preset

Returns:

Expression state configuration

struct Cheek

Cheek configuration (for shaping eye bottom)

Public Members

bool enabled = {false}

Whether to draw cheeks.

float size = {0.3f}

Cheek size relative to eye.

struct Config

Configuration for expressive eyes.

Public Members

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.

Blink duration in seconds.

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.

struct ExpressionState

Complete expression state.

Public Members

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.

struct Eyebrow

Eyebrow configuration.

Public Members

bool enabled = {false}

Whether to draw eyebrows.

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.

uint16_t color = {0x0000}

Eyebrow color (black)

struct EyeState

Single eye render data.

Public Members

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.

struct Pupil

Pupil configuration.

Public Members

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)