Files
tactility/Tactility/Source/service/displayidle/MatrixRainScreensaver.cpp
T
Shadowtrance c05d46a28c Screensavers (#462)
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 -->
2026-01-27 17:21:16 +01:00

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