c05d46a28c
Adds screensavers in addition to the backlight idle off.
More info in screensavers.md
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **New Features**
* Added a screensaver system with multiple styles (Bouncing Balls, Mystify, Matrix Rain) and a common screensaver interface; auto-starts after inactivity, dismisses on interaction, and supports auto-off/backlight behavior.
* New Display setting and UI dropdown to choose and persist the screensaver.
* **Documentation**
* Added comprehensive screensaver docs covering usage, extension, and configuration.
* **Chores**
* Registered the display-idle service.
* **Bug Fixes**
* Updated LVGL API calls to match renamed functions.
<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
321 lines
10 KiB
C++
321 lines
10 KiB
C++
#ifdef ESP_PLATFORM
|
|
|
|
#include "MatrixRainScreensaver.h"
|
|
#include <cstdlib>
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
|
|
namespace tt::service::displayidle {
|
|
|
|
static_assert(MatrixRainScreensaver::MAX_TRAIL_LENGTH > MatrixRainScreensaver::MIN_TRAIL_LENGTH,
|
|
"MAX_TRAIL_LENGTH must be greater than MIN_TRAIL_LENGTH");
|
|
static_assert(MatrixRainScreensaver::MAX_TICK_DELAY >= MatrixRainScreensaver::MIN_TICK_DELAY,
|
|
"MAX_TICK_DELAY must be >= MIN_TICK_DELAY");
|
|
|
|
static constexpr int TRAIL_LENGTH_RANGE = MatrixRainScreensaver::MAX_TRAIL_LENGTH - MatrixRainScreensaver::MIN_TRAIL_LENGTH;
|
|
static constexpr int TICK_DELAY_RANGE = MatrixRainScreensaver::MAX_TICK_DELAY - MatrixRainScreensaver::MIN_TICK_DELAY + 1;
|
|
|
|
char MatrixRainScreensaver::randomChar() {
|
|
static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*<>?";
|
|
return chars[rand() % (sizeof(chars) - 1)];
|
|
}
|
|
|
|
lv_color_t MatrixRainScreensaver::getTrailColor(int index, int trailLength) const {
|
|
if (trailLength <= 1) {
|
|
return lv_color_hex(COLOR_GRADIENT[0]);
|
|
}
|
|
|
|
float intensity = static_cast<float>(index) / static_cast<float>(trailLength - 1);
|
|
int colorIndex = static_cast<int>(intensity * (COLOR_GRADIENT.size() - 1) + 0.5f);
|
|
|
|
if (colorIndex < 0) colorIndex = 0;
|
|
if (colorIndex >= static_cast<int>(COLOR_GRADIENT.size())) {
|
|
colorIndex = COLOR_GRADIENT.size() - 1;
|
|
}
|
|
|
|
return lv_color_hex(COLOR_GRADIENT[colorIndex]);
|
|
}
|
|
|
|
int MatrixRainScreensaver::getRandomAvailableColumn() {
|
|
if (availableColumns_.empty()) {
|
|
return -1;
|
|
}
|
|
int idx = rand() % availableColumns_.size();
|
|
int column = availableColumns_[idx];
|
|
// O(1) removal: swap with last element and pop
|
|
availableColumns_[idx] = availableColumns_.back();
|
|
availableColumns_.pop_back();
|
|
return column;
|
|
}
|
|
|
|
void MatrixRainScreensaver::releaseColumn(int column) {
|
|
// Add column back to available pool if not already present (duplicate protection)
|
|
bool alreadyPresent = std::find(availableColumns_.begin(), availableColumns_.end(), column) != availableColumns_.end();
|
|
assert(!alreadyPresent && "Column released twice - indicates bug in column management");
|
|
if (!alreadyPresent) {
|
|
availableColumns_.push_back(column);
|
|
}
|
|
}
|
|
|
|
void MatrixRainScreensaver::createRainCharLabels(RainChar& rc, int trailIndex, int trailLength) {
|
|
if (overlay_ == nullptr) {
|
|
rc.label = nullptr;
|
|
rc.glowLabel = nullptr;
|
|
return;
|
|
}
|
|
rc.ch = randomChar();
|
|
|
|
// Create glow layer first (behind main text)
|
|
rc.glowLabel = lv_label_create(overlay_);
|
|
if (rc.glowLabel == nullptr) {
|
|
rc.label = nullptr;
|
|
return;
|
|
}
|
|
lv_obj_set_style_text_font(rc.glowLabel, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(rc.glowLabel, lv_color_hex(COLOR_GLOW), 0);
|
|
lv_obj_set_style_opa(rc.glowLabel, LV_OPA_70, 0);
|
|
|
|
// Create main character label
|
|
rc.label = lv_label_create(overlay_);
|
|
if (rc.label == nullptr) {
|
|
lv_obj_delete(rc.glowLabel);
|
|
rc.glowLabel = nullptr;
|
|
return;
|
|
}
|
|
lv_obj_set_style_text_font(rc.label, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(rc.label, getTrailColor(trailIndex, trailLength), 0);
|
|
|
|
char text[2] = {rc.ch, '\0'};
|
|
lv_label_set_text(rc.label, text);
|
|
lv_label_set_text(rc.glowLabel, text);
|
|
|
|
// Hide initially
|
|
lv_obj_set_pos(rc.label, -100, -100);
|
|
lv_obj_set_pos(rc.glowLabel, -100, -100);
|
|
}
|
|
|
|
void MatrixRainScreensaver::initRaindrop(Raindrop& drop, int column) {
|
|
drop.column = column;
|
|
drop.trailLength = MIN_TRAIL_LENGTH + rand() % TRAIL_LENGTH_RANGE;
|
|
drop.headRow = -(drop.trailLength); // Start above screen
|
|
drop.tickDelay = MIN_TICK_DELAY + rand() % TICK_DELAY_RANGE;
|
|
drop.tickCounter = 0;
|
|
drop.active = true;
|
|
|
|
// Create character labels for trail
|
|
drop.chars.resize(drop.trailLength);
|
|
for (int i = 0; i < drop.trailLength; i++) {
|
|
createRainCharLabels(drop.chars[i], i, drop.trailLength);
|
|
}
|
|
}
|
|
|
|
void MatrixRainScreensaver::resetRaindrop(Raindrop& drop) {
|
|
releaseColumn(drop.column);
|
|
|
|
int newColumn = getRandomAvailableColumn();
|
|
if (newColumn < 0) {
|
|
drop.active = false;
|
|
return;
|
|
}
|
|
|
|
drop.column = newColumn;
|
|
drop.tickDelay = MIN_TICK_DELAY + rand() % TICK_DELAY_RANGE;
|
|
drop.tickCounter = 0;
|
|
|
|
int newTrailLength = MIN_TRAIL_LENGTH + rand() % TRAIL_LENGTH_RANGE;
|
|
|
|
// Resize if needed
|
|
if (newTrailLength != drop.trailLength) {
|
|
// Delete excess labels
|
|
for (size_t i = newTrailLength; i < drop.chars.size(); i++) {
|
|
if (drop.chars[i].label) {
|
|
lv_obj_delete(drop.chars[i].label);
|
|
}
|
|
if (drop.chars[i].glowLabel) {
|
|
lv_obj_delete(drop.chars[i].glowLabel);
|
|
}
|
|
}
|
|
|
|
size_t oldSize = drop.chars.size();
|
|
drop.chars.resize(newTrailLength);
|
|
|
|
// Create new labels if needed (will be refreshed below)
|
|
for (size_t i = oldSize; i < drop.chars.size(); i++) {
|
|
createRainCharLabels(drop.chars[i], static_cast<int>(i), newTrailLength);
|
|
}
|
|
|
|
drop.trailLength = newTrailLength;
|
|
}
|
|
|
|
// Reset position above screen
|
|
drop.headRow = -(drop.trailLength);
|
|
|
|
// Refresh characters and colors for all labels
|
|
for (int i = 0; i < drop.trailLength; i++) {
|
|
auto& rc = drop.chars[i];
|
|
if (rc.label == nullptr || rc.glowLabel == nullptr) {
|
|
continue;
|
|
}
|
|
rc.ch = randomChar();
|
|
|
|
char text[2] = {rc.ch, '\0'};
|
|
lv_label_set_text(rc.label, text);
|
|
lv_label_set_text(rc.glowLabel, text);
|
|
lv_obj_set_style_text_color(rc.label, getTrailColor(i, drop.trailLength), 0);
|
|
|
|
lv_obj_set_pos(rc.label, -100, -100);
|
|
lv_obj_set_pos(rc.glowLabel, -100, -100);
|
|
}
|
|
|
|
drop.active = true;
|
|
}
|
|
|
|
void MatrixRainScreensaver::updateRaindropDisplay(Raindrop& drop) {
|
|
int xPos = drop.column * CHAR_WIDTH;
|
|
|
|
for (int i = 0; i < drop.trailLength; i++) {
|
|
auto& rc = drop.chars[i];
|
|
if (rc.label == nullptr || rc.glowLabel == nullptr) {
|
|
continue;
|
|
}
|
|
int row = drop.headRow - i;
|
|
int yPos = row * CHAR_HEIGHT;
|
|
|
|
// Only show characters that are on screen (grid-snapped positions)
|
|
if (row >= 0 && row < numRows_) {
|
|
lv_obj_set_pos(rc.label, xPos, yPos);
|
|
lv_obj_set_pos(rc.glowLabel, xPos + GLOW_OFFSET, yPos + GLOW_OFFSET);
|
|
} else {
|
|
lv_obj_set_pos(rc.label, -100, -100);
|
|
lv_obj_set_pos(rc.glowLabel, -100, -100);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MatrixRainScreensaver::flickerRandomChars() {
|
|
constexpr int ERROR_PERCENT = 3;
|
|
|
|
for (auto& drop : drops_) {
|
|
if (!drop.active) continue;
|
|
|
|
for (int i = 0; i < drop.trailLength; i++) {
|
|
int row = drop.headRow - i;
|
|
|
|
// Only process visible characters
|
|
if (row >= 0 && row < numRows_) {
|
|
if (rand() % 100 < ERROR_PERCENT) {
|
|
auto& rc = drop.chars[i];
|
|
if (rc.label == nullptr || rc.glowLabel == nullptr) {
|
|
continue;
|
|
}
|
|
rc.ch = randomChar();
|
|
char text[2] = {rc.ch, '\0'};
|
|
lv_label_set_text(rc.label, text);
|
|
lv_label_set_text(rc.glowLabel, text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MatrixRainScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) {
|
|
if (!drops_.empty()) {
|
|
stop();
|
|
}
|
|
|
|
overlay_ = overlay;
|
|
screenW_ = screenW;
|
|
screenH_ = screenH;
|
|
globalFlickerCounter_ = 0;
|
|
|
|
numColumns_ = screenW / CHAR_WIDTH;
|
|
numRows_ = screenH / CHAR_HEIGHT;
|
|
|
|
availableColumns_.clear();
|
|
for (int i = 0; i < numColumns_; i++) {
|
|
availableColumns_.push_back(i);
|
|
}
|
|
|
|
int numDrops = std::min(MAX_ACTIVE_DROPS, numColumns_);
|
|
drops_.resize(numDrops);
|
|
|
|
for (int i = 0; i < numDrops; i++) {
|
|
int col = getRandomAvailableColumn();
|
|
if (col >= 0) {
|
|
initRaindrop(drops_[i], col);
|
|
// Stagger start positions (guard against tiny screens)
|
|
int staggerRange = numRows_ / 2;
|
|
if (staggerRange > 0) {
|
|
drops_[i].headRow = -(rand() % staggerRange);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MatrixRainScreensaver::stop() {
|
|
// Explicitly delete labels to prevent resource leaks
|
|
for (auto& drop : drops_) {
|
|
for (auto& rc : drop.chars) {
|
|
if (rc.label) {
|
|
lv_obj_delete(rc.label);
|
|
rc.label = nullptr;
|
|
}
|
|
if (rc.glowLabel) {
|
|
lv_obj_delete(rc.glowLabel);
|
|
rc.glowLabel = nullptr;
|
|
}
|
|
}
|
|
drop.chars.clear();
|
|
}
|
|
drops_.clear();
|
|
availableColumns_.clear();
|
|
overlay_ = nullptr;
|
|
}
|
|
|
|
void MatrixRainScreensaver::update(lv_coord_t screenW, lv_coord_t screenH) {
|
|
// Screen dimensions captured at start() - no dynamic resize support during screensaver
|
|
LV_UNUSED(screenW);
|
|
LV_UNUSED(screenH);
|
|
|
|
// Global glitch effect
|
|
globalFlickerCounter_++;
|
|
if (globalFlickerCounter_ >= 5) {
|
|
globalFlickerCounter_ = 0;
|
|
flickerRandomChars();
|
|
}
|
|
|
|
for (auto& drop : drops_) {
|
|
if (!drop.active) continue;
|
|
|
|
// Tick-based movement (terminal style - advance by row)
|
|
drop.tickCounter++;
|
|
if (drop.tickCounter >= drop.tickDelay) {
|
|
drop.tickCounter = 0;
|
|
|
|
// Advance head down by one row
|
|
drop.headRow++;
|
|
|
|
// Randomly change the head character when it advances
|
|
if (!drop.chars.empty() && drop.chars[0].label != nullptr && drop.chars[0].glowLabel != nullptr) {
|
|
drop.chars[0].ch = randomChar();
|
|
char text[2] = {drop.chars[0].ch, '\0'};
|
|
lv_label_set_text(drop.chars[0].label, text);
|
|
lv_label_set_text(drop.chars[0].glowLabel, text);
|
|
}
|
|
|
|
// Check if tail has completely left the screen
|
|
int tailRow = drop.headRow - (drop.trailLength - 1);
|
|
if (tailRow >= numRows_) {
|
|
resetRaindrop(drop);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
updateRaindropDisplay(drop);
|
|
}
|
|
}
|
|
|
|
} // namespace tt::service::displayidle
|
|
|
|
#endif // ESP_PLATFORM
|