feat: Sleep screen letterbox fill and image upscaling
Add configurable letterbox fill for sleep screen cover images that don't match the display aspect ratio. Four fill modes are available: Solid (single dominant edge shade), Blended (per-pixel edge colors), Gradient (edge colors interpolated toward white/black), and None. Enable upscaling of cover images smaller than the display in Fit mode by modifying drawBitmap/drawBitmap1Bit to support both up and downscaling via a unified block-fill approach. Edge sampling data is cached to .crosspoint alongside the cover BMP to avoid redundant bitmap scanning on subsequent sleeps. Cache is validated against screen dimensions and auto-regenerated when stale. New settings: Letterbox Fill (None/Solid/Blended/Gradient) and Gradient Direction (To White/To Black). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -104,3 +104,20 @@ uint8_t quantize1bit(int gray, int x, int y) {
|
|||||||
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
|
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
|
||||||
return (gray >= adjustedThreshold) ? 1 : 0;
|
return (gray >= adjustedThreshold) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Noise dithering for gradient fills - always uses hash-based noise regardless of global dithering config.
|
||||||
|
// Produces smooth-looking gradients on the 4-level e-ink display.
|
||||||
|
uint8_t quantizeNoiseDither(int gray, int x, int y) {
|
||||||
|
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
||||||
|
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||||
|
const int threshold = static_cast<int>(hash >> 24);
|
||||||
|
|
||||||
|
const int scaled = gray * 3;
|
||||||
|
if (scaled < 255) {
|
||||||
|
return (scaled + threshold >= 255) ? 1 : 0;
|
||||||
|
} else if (scaled < 510) {
|
||||||
|
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
||||||
|
} else {
|
||||||
|
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ uint8_t quantize(int gray, int x, int y);
|
|||||||
uint8_t quantizeSimple(int gray);
|
uint8_t quantizeSimple(int gray);
|
||||||
uint8_t quantize1bit(int gray, int x, int y);
|
uint8_t quantize1bit(int gray, int x, int y);
|
||||||
int adjustPixel(int gray);
|
int adjustPixel(int gray);
|
||||||
|
uint8_t quantizeNoiseDither(int gray, int x, int y);
|
||||||
|
|
||||||
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
|
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
|
||||||
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
|
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
|
||||||
|
|||||||
@@ -72,6 +72,16 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawPixelGray(const int x, const int y, const uint8_t val2bit) const {
|
||||||
|
if (renderMode == BW && val2bit < 3) {
|
||||||
|
drawPixel(x, y);
|
||||||
|
} else if (renderMode == GRAYSCALE_MSB && (val2bit == 1 || val2bit == 2)) {
|
||||||
|
drawPixel(x, y, false);
|
||||||
|
} else if (renderMode == GRAYSCALE_LSB && val2bit == 1) {
|
||||||
|
drawPixel(x, y, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||||
@@ -422,12 +432,20 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
||||||
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||||
|
|
||||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth();
|
||||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight();
|
||||||
|
|
||||||
|
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
||||||
|
if (maxWidth > 0 && maxHeight > 0) {
|
||||||
|
const float scaleX = static_cast<float>(maxWidth) / effectiveWidth;
|
||||||
|
const float scaleY = static_cast<float>(maxHeight) / effectiveHeight;
|
||||||
|
scale = std::min(scaleX, scaleY);
|
||||||
|
isScaled = (scale < 0.999f || scale > 1.001f);
|
||||||
|
} else if (maxWidth > 0 && effectiveWidth > static_cast<float>(maxWidth)) {
|
||||||
|
scale = static_cast<float>(maxWidth) / effectiveWidth;
|
||||||
isScaled = true;
|
isScaled = true;
|
||||||
}
|
} else if (maxHeight > 0 && effectiveHeight > static_cast<float>(maxHeight)) {
|
||||||
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
scale = static_cast<float>(maxHeight) / effectiveHeight;
|
||||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
|
||||||
isScaled = true;
|
isScaled = true;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
||||||
@@ -448,12 +466,17 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
||||||
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
||||||
// Screen's (0, 0) is the top-left corner.
|
// Screen's (0, 0) is the top-left corner.
|
||||||
int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
const int logicalY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||||
|
int screenYStart, screenYEnd;
|
||||||
if (isScaled) {
|
if (isScaled) {
|
||||||
screenY = std::floor(screenY * scale);
|
screenYStart = static_cast<int>(std::floor(logicalY * scale)) + y;
|
||||||
|
screenYEnd = static_cast<int>(std::floor((logicalY + 1) * scale)) + y;
|
||||||
|
} else {
|
||||||
|
screenYStart = logicalY + y;
|
||||||
|
screenYEnd = screenYStart + 1;
|
||||||
}
|
}
|
||||||
screenY += y; // the offset should not be scaled
|
|
||||||
if (screenY >= getScreenHeight()) {
|
if (screenYStart >= getScreenHeight()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +487,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenY < 0) {
|
if (screenYEnd <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,27 +496,42 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int syStart = std::max(screenYStart, 0);
|
||||||
|
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
||||||
|
|
||||||
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||||
int screenX = bmpX - cropPixX;
|
const int outX = bmpX - cropPixX;
|
||||||
|
int screenXStart, screenXEnd;
|
||||||
if (isScaled) {
|
if (isScaled) {
|
||||||
screenX = std::floor(screenX * scale);
|
screenXStart = static_cast<int>(std::floor(outX * scale)) + x;
|
||||||
|
screenXEnd = static_cast<int>(std::floor((outX + 1) * scale)) + x;
|
||||||
|
} else {
|
||||||
|
screenXStart = outX + x;
|
||||||
|
screenXEnd = screenXStart + 1;
|
||||||
}
|
}
|
||||||
screenX += x; // the offset should not be scaled
|
|
||||||
if (screenX >= getScreenWidth()) {
|
if (screenXStart >= getScreenWidth()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (screenX < 0) {
|
if (screenXEnd <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
||||||
|
|
||||||
if (renderMode == BW && val < 3) {
|
const int sxStart = std::max(screenXStart, 0);
|
||||||
drawPixel(screenX, screenY);
|
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
||||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
|
||||||
drawPixel(screenX, screenY, false);
|
for (int sy = syStart; sy < syEnd; sy++) {
|
||||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
for (int sx = sxStart; sx < sxEnd; sx++) {
|
||||||
drawPixel(screenX, screenY, false);
|
if (renderMode == BW && val < 3) {
|
||||||
|
drawPixel(sx, sy);
|
||||||
|
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
||||||
|
drawPixel(sx, sy, false);
|
||||||
|
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||||
|
drawPixel(sx, sy, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,11 +544,16 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
const int maxHeight) const {
|
const int maxHeight) const {
|
||||||
float scale = 1.0f;
|
float scale = 1.0f;
|
||||||
bool isScaled = false;
|
bool isScaled = false;
|
||||||
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
||||||
|
if (maxWidth > 0 && maxHeight > 0) {
|
||||||
|
const float scaleX = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||||
|
const float scaleY = static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight());
|
||||||
|
scale = std::min(scaleX, scaleY);
|
||||||
|
isScaled = (scale < 0.999f || scale > 1.001f);
|
||||||
|
} else if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
||||||
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||||
isScaled = true;
|
isScaled = true;
|
||||||
}
|
} else if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||||
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
|
||||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
||||||
isScaled = true;
|
isScaled = true;
|
||||||
}
|
}
|
||||||
@@ -538,20 +581,37 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
|
|
||||||
// Calculate screen Y based on whether BMP is top-down or bottom-up
|
// Calculate screen Y based on whether BMP is top-down or bottom-up
|
||||||
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||||
int screenY = y + (isScaled ? static_cast<int>(std::floor(bmpYOffset * scale)) : bmpYOffset);
|
int screenYStart, screenYEnd;
|
||||||
if (screenY >= getScreenHeight()) {
|
if (isScaled) {
|
||||||
|
screenYStart = static_cast<int>(std::floor(bmpYOffset * scale)) + y;
|
||||||
|
screenYEnd = static_cast<int>(std::floor((bmpYOffset + 1) * scale)) + y;
|
||||||
|
} else {
|
||||||
|
screenYStart = bmpYOffset + y;
|
||||||
|
screenYEnd = screenYStart + 1;
|
||||||
|
}
|
||||||
|
if (screenYStart >= getScreenHeight()) {
|
||||||
continue; // Continue reading to keep row counter in sync
|
continue; // Continue reading to keep row counter in sync
|
||||||
}
|
}
|
||||||
if (screenY < 0) {
|
if (screenYEnd <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int syStart = std::max(screenYStart, 0);
|
||||||
|
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
||||||
|
|
||||||
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
||||||
int screenX = x + (isScaled ? static_cast<int>(std::floor(bmpX * scale)) : bmpX);
|
int screenXStart, screenXEnd;
|
||||||
if (screenX >= getScreenWidth()) {
|
if (isScaled) {
|
||||||
|
screenXStart = static_cast<int>(std::floor(bmpX * scale)) + x;
|
||||||
|
screenXEnd = static_cast<int>(std::floor((bmpX + 1) * scale)) + x;
|
||||||
|
} else {
|
||||||
|
screenXStart = bmpX + x;
|
||||||
|
screenXEnd = screenXStart + 1;
|
||||||
|
}
|
||||||
|
if (screenXStart >= getScreenWidth()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (screenX < 0) {
|
if (screenXEnd <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +621,13 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
|
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
|
||||||
// val < 3 means black pixel (draw it)
|
// val < 3 means black pixel (draw it)
|
||||||
if (val < 3) {
|
if (val < 3) {
|
||||||
drawPixel(screenX, screenY, true);
|
const int sxStart = std::max(screenXStart, 0);
|
||||||
|
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
||||||
|
for (int sy = syStart; sy < syEnd; sy++) {
|
||||||
|
for (int sx = sxStart; sx < sxEnd; sx++) {
|
||||||
|
drawPixel(sx, sy, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// White pixels (val == 3) are not drawn (leave background)
|
// White pixels (val == 3) are not drawn (leave background)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class GfxRenderer {
|
|||||||
|
|
||||||
// Drawing
|
// Drawing
|
||||||
void drawPixel(int x, int y, bool state = true) const;
|
void drawPixel(int x, int y, bool state = true) const;
|
||||||
|
void drawPixelGray(int x, int y, uint8_t val2bit) const;
|
||||||
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||||
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
|
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
|
||||||
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// Increment this when adding new persisted settings fields
|
||||||
constexpr uint8_t SETTINGS_COUNT = 30;
|
constexpr uint8_t SETTINGS_COUNT = 32;
|
||||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
|
|
||||||
// Validate front button mapping to ensure each hardware button is unique.
|
// Validate front button mapping to ensure each hardware button is unique.
|
||||||
@@ -118,6 +118,8 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, frontButtonRight);
|
serialization::writePod(outputFile, frontButtonRight);
|
||||||
serialization::writePod(outputFile, fadingFix);
|
serialization::writePod(outputFile, fadingFix);
|
||||||
serialization::writePod(outputFile, embeddedStyle);
|
serialization::writePod(outputFile, embeddedStyle);
|
||||||
|
serialization::writePod(outputFile, sleepScreenLetterboxFill);
|
||||||
|
serialization::writePod(outputFile, sleepScreenGradientDir);
|
||||||
// New fields added at end for backward compatibility
|
// New fields added at end for backward compatibility
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
@@ -223,6 +225,10 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
serialization::readPod(inputFile, embeddedStyle);
|
serialization::readPod(inputFile, embeddedStyle);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
readAndValidate(inputFile, sleepScreenLetterboxFill, SLEEP_SCREEN_LETTERBOX_FILL_COUNT);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
readAndValidate(inputFile, sleepScreenGradientDir, SLEEP_SCREEN_GRADIENT_DIR_COUNT);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
// New fields added at end for backward compatibility
|
// New fields added at end for backward compatibility
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ class CrossPointSettings {
|
|||||||
INVERTED_BLACK_AND_WHITE = 2,
|
INVERTED_BLACK_AND_WHITE = 2,
|
||||||
SLEEP_SCREEN_COVER_FILTER_COUNT
|
SLEEP_SCREEN_COVER_FILTER_COUNT
|
||||||
};
|
};
|
||||||
|
enum SLEEP_SCREEN_LETTERBOX_FILL {
|
||||||
|
LETTERBOX_NONE = 0,
|
||||||
|
LETTERBOX_SOLID = 1,
|
||||||
|
LETTERBOX_BLENDED = 2,
|
||||||
|
LETTERBOX_GRADIENT = 3,
|
||||||
|
SLEEP_SCREEN_LETTERBOX_FILL_COUNT
|
||||||
|
};
|
||||||
|
enum SLEEP_SCREEN_GRADIENT_DIR { GRADIENT_TO_WHITE = 0, GRADIENT_TO_BLACK = 1, SLEEP_SCREEN_GRADIENT_DIR_COUNT };
|
||||||
|
|
||||||
// Status bar display type enum
|
// Status bar display type enum
|
||||||
enum STATUS_BAR_MODE {
|
enum STATUS_BAR_MODE {
|
||||||
@@ -125,6 +133,10 @@ class CrossPointSettings {
|
|||||||
uint8_t sleepScreenCoverMode = FIT;
|
uint8_t sleepScreenCoverMode = FIT;
|
||||||
// Sleep screen cover filter
|
// Sleep screen cover filter
|
||||||
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
||||||
|
// Sleep screen letterbox fill mode (None / Solid / Blended / Gradient)
|
||||||
|
uint8_t sleepScreenLetterboxFill = LETTERBOX_GRADIENT;
|
||||||
|
// Sleep screen gradient direction (towards white or black)
|
||||||
|
uint8_t sleepScreenGradientDir = GRADIENT_TO_WHITE;
|
||||||
// Status bar settings
|
// Status bar settings
|
||||||
uint8_t statusBar = FULL;
|
uint8_t statusBar = FULL;
|
||||||
// Text rendering settings
|
// Text rendering settings
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ inline std::vector<SettingInfo> getSettingsList() {
|
|||||||
"sleepScreenCoverMode", "Display"),
|
"sleepScreenCoverMode", "Display"),
|
||||||
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
|
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
|
||||||
{"None", "Contrast", "Inverted"}, "sleepScreenCoverFilter", "Display"),
|
{"None", "Contrast", "Inverted"}, "sleepScreenCoverFilter", "Display"),
|
||||||
|
SettingInfo::Enum("Letterbox Fill", &CrossPointSettings::sleepScreenLetterboxFill,
|
||||||
|
{"None", "Solid", "Blended", "Gradient"}, "sleepScreenLetterboxFill", "Display"),
|
||||||
|
SettingInfo::Enum("Gradient Direction", &CrossPointSettings::sleepScreenGradientDir, {"To White", "To Black"},
|
||||||
|
"sleepScreenGradientDir", "Display"),
|
||||||
SettingInfo::Enum(
|
SettingInfo::Enum(
|
||||||
"Status Bar", &CrossPointSettings::statusBar,
|
"Status Bar", &CrossPointSettings::statusBar,
|
||||||
{"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"},
|
{"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"},
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
#include "SleepActivity.h"
|
#include "SleepActivity.h"
|
||||||
|
|
||||||
|
#include <BitmapHelpers.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Serialization.h>
|
||||||
#include <Txt.h>
|
#include <Txt.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
@@ -13,6 +17,364 @@
|
|||||||
#include "images/Logo120.h"
|
#include "images/Logo120.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Number of source pixels along the image edge to average for the gradient color
|
||||||
|
constexpr int EDGE_SAMPLE_DEPTH = 20;
|
||||||
|
|
||||||
|
// Map a 2-bit quantized pixel value to an 8-bit grayscale value
|
||||||
|
constexpr uint8_t val2bitToGray(uint8_t val2bit) { return val2bit * 85; }
|
||||||
|
|
||||||
|
// Edge gradient data produced by sampleBitmapEdges and consumed by drawLetterboxGradients.
|
||||||
|
// edgeA is the "first" edge (top or left), edgeB is the "second" edge (bottom or right).
|
||||||
|
struct LetterboxGradientData {
|
||||||
|
uint8_t* edgeA = nullptr;
|
||||||
|
uint8_t* edgeB = nullptr;
|
||||||
|
int edgeCount = 0;
|
||||||
|
int letterboxA = 0; // pixel size of the first letterbox area (top or left)
|
||||||
|
int letterboxB = 0; // pixel size of the second letterbox area (bottom or right)
|
||||||
|
bool horizontal = false; // true = top/bottom letterbox, false = left/right
|
||||||
|
|
||||||
|
void free() {
|
||||||
|
::free(edgeA);
|
||||||
|
::free(edgeB);
|
||||||
|
edgeA = nullptr;
|
||||||
|
edgeB = nullptr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Binary cache version for edge data files
|
||||||
|
constexpr uint8_t EDGE_CACHE_VERSION = 1;
|
||||||
|
|
||||||
|
// Load cached edge data from a binary file. Returns true if the cache was valid and loaded successfully.
|
||||||
|
// Validates cache version and screen dimensions to detect stale data.
|
||||||
|
bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxGradientData& data) {
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("SLP", path, file)) return false;
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(file, version);
|
||||||
|
if (version != EDGE_CACHE_VERSION) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedW, cachedH;
|
||||||
|
serialization::readPod(file, cachedW);
|
||||||
|
serialization::readPod(file, cachedH);
|
||||||
|
if (cachedW != static_cast<uint16_t>(screenWidth) || cachedH != static_cast<uint16_t>(screenHeight)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t horizontal;
|
||||||
|
serialization::readPod(file, horizontal);
|
||||||
|
data.horizontal = (horizontal != 0);
|
||||||
|
|
||||||
|
uint16_t edgeCount;
|
||||||
|
serialization::readPod(file, edgeCount);
|
||||||
|
data.edgeCount = edgeCount;
|
||||||
|
|
||||||
|
int16_t lbA, lbB;
|
||||||
|
serialization::readPod(file, lbA);
|
||||||
|
serialization::readPod(file, lbB);
|
||||||
|
data.letterboxA = lbA;
|
||||||
|
data.letterboxB = lbB;
|
||||||
|
|
||||||
|
if (edgeCount == 0 || edgeCount > 2048) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.edgeA = static_cast<uint8_t*>(malloc(edgeCount));
|
||||||
|
data.edgeB = static_cast<uint8_t*>(malloc(edgeCount));
|
||||||
|
if (!data.edgeA || !data.edgeB) {
|
||||||
|
data.free();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.read(data.edgeA, edgeCount) != static_cast<int>(edgeCount) ||
|
||||||
|
file.read(data.edgeB, edgeCount) != static_cast<int>(edgeCount)) {
|
||||||
|
data.free();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
Serial.printf("[%lu] [SLP] Loaded edge cache from %s (%d edges)\n", millis(), path.c_str(), edgeCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save edge data to a binary cache file for reuse on subsequent sleep screens.
|
||||||
|
bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxGradientData& data) {
|
||||||
|
if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return false;
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForWrite("SLP", path, file)) return false;
|
||||||
|
|
||||||
|
serialization::writePod(file, EDGE_CACHE_VERSION);
|
||||||
|
serialization::writePod(file, static_cast<uint16_t>(screenWidth));
|
||||||
|
serialization::writePod(file, static_cast<uint16_t>(screenHeight));
|
||||||
|
serialization::writePod(file, static_cast<uint8_t>(data.horizontal ? 1 : 0));
|
||||||
|
serialization::writePod(file, static_cast<uint16_t>(data.edgeCount));
|
||||||
|
serialization::writePod(file, static_cast<int16_t>(data.letterboxA));
|
||||||
|
serialization::writePod(file, static_cast<int16_t>(data.letterboxB));
|
||||||
|
file.write(data.edgeA, data.edgeCount);
|
||||||
|
file.write(data.edgeB, data.edgeCount);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [SLP] Saved edge cache to %s (%d edges)\n", millis(), path.c_str(), data.edgeCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the bitmap once to sample the first/last EDGE_SAMPLE_DEPTH rows or columns.
|
||||||
|
// Returns edge color arrays in source pixel resolution. Caller must call data.free() when done.
|
||||||
|
// After sampling the bitmap is rewound via rewindToData().
|
||||||
|
LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight,
|
||||||
|
float scale, float cropX, float cropY) {
|
||||||
|
LetterboxGradientData data;
|
||||||
|
|
||||||
|
const int cropPixX = static_cast<int>(std::floor(bitmap.getWidth() * cropX / 2.0f));
|
||||||
|
const int cropPixY = static_cast<int>(std::floor(bitmap.getHeight() * cropY / 2.0f));
|
||||||
|
const int visibleWidth = bitmap.getWidth() - 2 * cropPixX;
|
||||||
|
const int visibleHeight = bitmap.getHeight() - 2 * cropPixY;
|
||||||
|
|
||||||
|
if (visibleWidth <= 0 || visibleHeight <= 0) return data;
|
||||||
|
|
||||||
|
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
|
||||||
|
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
||||||
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||||
|
if (!outputRow || !rowBytes) {
|
||||||
|
::free(outputRow);
|
||||||
|
::free(rowBytes);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imgY > 0) {
|
||||||
|
// Top/bottom letterboxing -- sample per-column averages of first/last N rows
|
||||||
|
data.horizontal = true;
|
||||||
|
data.edgeCount = visibleWidth;
|
||||||
|
const int scaledHeight = static_cast<int>(std::round(static_cast<float>(visibleHeight) * scale));
|
||||||
|
data.letterboxA = imgY;
|
||||||
|
data.letterboxB = pageHeight - imgY - scaledHeight;
|
||||||
|
if (data.letterboxB < 0) data.letterboxB = 0;
|
||||||
|
|
||||||
|
const int sampleRows = std::min(EDGE_SAMPLE_DEPTH, visibleHeight);
|
||||||
|
|
||||||
|
auto* accumTop = static_cast<uint32_t*>(calloc(visibleWidth, sizeof(uint32_t)));
|
||||||
|
auto* accumBot = static_cast<uint32_t*>(calloc(visibleWidth, sizeof(uint32_t)));
|
||||||
|
data.edgeA = static_cast<uint8_t*>(malloc(visibleWidth));
|
||||||
|
data.edgeB = static_cast<uint8_t*>(malloc(visibleWidth));
|
||||||
|
|
||||||
|
if (!accumTop || !accumBot || !data.edgeA || !data.edgeB) {
|
||||||
|
::free(accumTop);
|
||||||
|
::free(accumBot);
|
||||||
|
data.free();
|
||||||
|
::free(outputRow);
|
||||||
|
::free(rowBytes);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||||
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break;
|
||||||
|
const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||||
|
if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue;
|
||||||
|
const int outY = logicalY - cropPixY;
|
||||||
|
|
||||||
|
const bool inTop = (outY < sampleRows);
|
||||||
|
const bool inBot = (outY >= visibleHeight - sampleRows);
|
||||||
|
if (!inTop && !inBot) continue;
|
||||||
|
|
||||||
|
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||||
|
const int outX = bmpX - cropPixX;
|
||||||
|
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
||||||
|
const uint8_t gray = val2bitToGray(val);
|
||||||
|
if (inTop) accumTop[outX] += gray;
|
||||||
|
if (inBot) accumBot[outX] += gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < visibleWidth; i++) {
|
||||||
|
data.edgeA[i] = static_cast<uint8_t>(accumTop[i] / sampleRows);
|
||||||
|
data.edgeB[i] = static_cast<uint8_t>(accumBot[i] / sampleRows);
|
||||||
|
}
|
||||||
|
::free(accumTop);
|
||||||
|
::free(accumBot);
|
||||||
|
|
||||||
|
} else if (imgX > 0) {
|
||||||
|
// Left/right letterboxing -- sample per-row averages of first/last N columns
|
||||||
|
data.horizontal = false;
|
||||||
|
data.edgeCount = visibleHeight;
|
||||||
|
const int scaledWidth = static_cast<int>(std::round(static_cast<float>(visibleWidth) * scale));
|
||||||
|
data.letterboxA = imgX;
|
||||||
|
data.letterboxB = pageWidth - imgX - scaledWidth;
|
||||||
|
if (data.letterboxB < 0) data.letterboxB = 0;
|
||||||
|
|
||||||
|
const int sampleCols = std::min(EDGE_SAMPLE_DEPTH, visibleWidth);
|
||||||
|
|
||||||
|
auto* accumLeft = static_cast<uint32_t*>(calloc(visibleHeight, sizeof(uint32_t)));
|
||||||
|
auto* accumRight = static_cast<uint32_t*>(calloc(visibleHeight, sizeof(uint32_t)));
|
||||||
|
data.edgeA = static_cast<uint8_t*>(malloc(visibleHeight));
|
||||||
|
data.edgeB = static_cast<uint8_t*>(malloc(visibleHeight));
|
||||||
|
|
||||||
|
if (!accumLeft || !accumRight || !data.edgeA || !data.edgeB) {
|
||||||
|
::free(accumLeft);
|
||||||
|
::free(accumRight);
|
||||||
|
data.free();
|
||||||
|
::free(outputRow);
|
||||||
|
::free(rowBytes);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||||
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break;
|
||||||
|
const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||||
|
if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue;
|
||||||
|
const int outY = logicalY - cropPixY;
|
||||||
|
|
||||||
|
// Sample left edge columns
|
||||||
|
for (int bmpX = cropPixX; bmpX < cropPixX + sampleCols; bmpX++) {
|
||||||
|
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
||||||
|
accumLeft[outY] += val2bitToGray(val);
|
||||||
|
}
|
||||||
|
// Sample right edge columns
|
||||||
|
for (int bmpX = bitmap.getWidth() - cropPixX - sampleCols; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||||
|
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
||||||
|
accumRight[outY] += val2bitToGray(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < visibleHeight; i++) {
|
||||||
|
data.edgeA[i] = static_cast<uint8_t>(accumLeft[i] / sampleCols);
|
||||||
|
data.edgeB[i] = static_cast<uint8_t>(accumRight[i] / sampleCols);
|
||||||
|
}
|
||||||
|
::free(accumLeft);
|
||||||
|
::free(accumRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
::free(outputRow);
|
||||||
|
::free(rowBytes);
|
||||||
|
bitmap.rewindToData();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw dithered fills in the letterbox areas using the sampled edge colors.
|
||||||
|
// fillMode selects the fill algorithm: SOLID (single dominant shade), BLENDED (per-pixel edge color),
|
||||||
|
// or GRADIENT (per-pixel edge color interpolated toward targetColor).
|
||||||
|
// targetColor is the color the gradient fades toward (255=white, 0=black); only used in GRADIENT mode.
|
||||||
|
// Must be called once per render pass (BW, GRAYSCALE_LSB, GRAYSCALE_MSB).
|
||||||
|
void drawLetterboxFill(GfxRenderer& renderer, const LetterboxGradientData& data, float scale, uint8_t fillMode,
|
||||||
|
int targetColor) {
|
||||||
|
if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return;
|
||||||
|
|
||||||
|
const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID);
|
||||||
|
const bool isGradient = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_GRADIENT);
|
||||||
|
|
||||||
|
// For SOLID mode, compute the dominant (average) shade for each edge once
|
||||||
|
uint8_t solidColorA = 0, solidColorB = 0;
|
||||||
|
if (isSolid) {
|
||||||
|
uint32_t sumA = 0, sumB = 0;
|
||||||
|
for (int i = 0; i < data.edgeCount; i++) {
|
||||||
|
sumA += data.edgeA[i];
|
||||||
|
sumB += data.edgeB[i];
|
||||||
|
}
|
||||||
|
solidColorA = static_cast<uint8_t>(sumA / data.edgeCount);
|
||||||
|
solidColorB = static_cast<uint8_t>(sumB / data.edgeCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: compute gray value for a pixel given the edge color and interpolation factor t (0..1)
|
||||||
|
// GRADIENT interpolates from edgeColor toward targetColor; SOLID and BLENDED return edgeColor directly.
|
||||||
|
auto computeGray = [&](int edgeColor, float t) -> int {
|
||||||
|
if (isGradient) return edgeColor + static_cast<int>(static_cast<float>(targetColor - edgeColor) * t);
|
||||||
|
return edgeColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.horizontal) {
|
||||||
|
// Top letterbox
|
||||||
|
if (data.letterboxA > 0) {
|
||||||
|
const int imgTopY = data.letterboxA;
|
||||||
|
for (int screenY = 0; screenY < imgTopY; screenY++) {
|
||||||
|
const float t = static_cast<float>(imgTopY - screenY) / static_cast<float>(imgTopY);
|
||||||
|
for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) {
|
||||||
|
int edgeColor;
|
||||||
|
if (isSolid) {
|
||||||
|
edgeColor = solidColorA;
|
||||||
|
} else {
|
||||||
|
int srcCol = static_cast<int>(screenX / scale);
|
||||||
|
srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1));
|
||||||
|
edgeColor = data.edgeA[srcCol];
|
||||||
|
}
|
||||||
|
const int gray = computeGray(edgeColor, t);
|
||||||
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom letterbox
|
||||||
|
if (data.letterboxB > 0) {
|
||||||
|
const int imgBottomY = renderer.getScreenHeight() - data.letterboxB;
|
||||||
|
for (int screenY = imgBottomY; screenY < renderer.getScreenHeight(); screenY++) {
|
||||||
|
const float t = static_cast<float>(screenY - imgBottomY + 1) / static_cast<float>(data.letterboxB);
|
||||||
|
for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) {
|
||||||
|
int edgeColor;
|
||||||
|
if (isSolid) {
|
||||||
|
edgeColor = solidColorB;
|
||||||
|
} else {
|
||||||
|
int srcCol = static_cast<int>(screenX / scale);
|
||||||
|
srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1));
|
||||||
|
edgeColor = data.edgeB[srcCol];
|
||||||
|
}
|
||||||
|
const int gray = computeGray(edgeColor, t);
|
||||||
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Left letterbox
|
||||||
|
if (data.letterboxA > 0) {
|
||||||
|
const int imgLeftX = data.letterboxA;
|
||||||
|
for (int screenX = 0; screenX < imgLeftX; screenX++) {
|
||||||
|
const float t = static_cast<float>(imgLeftX - screenX) / static_cast<float>(imgLeftX);
|
||||||
|
for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) {
|
||||||
|
int edgeColor;
|
||||||
|
if (isSolid) {
|
||||||
|
edgeColor = solidColorA;
|
||||||
|
} else {
|
||||||
|
int srcRow = static_cast<int>(screenY / scale);
|
||||||
|
srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1));
|
||||||
|
edgeColor = data.edgeA[srcRow];
|
||||||
|
}
|
||||||
|
const int gray = computeGray(edgeColor, t);
|
||||||
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right letterbox
|
||||||
|
if (data.letterboxB > 0) {
|
||||||
|
const int imgRightX = renderer.getScreenWidth() - data.letterboxB;
|
||||||
|
for (int screenX = imgRightX; screenX < renderer.getScreenWidth(); screenX++) {
|
||||||
|
const float t = static_cast<float>(screenX - imgRightX + 1) / static_cast<float>(data.letterboxB);
|
||||||
|
for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) {
|
||||||
|
int edgeColor;
|
||||||
|
if (isSolid) {
|
||||||
|
edgeColor = solidColorB;
|
||||||
|
} else {
|
||||||
|
int srcRow = static_cast<int>(screenY / scale);
|
||||||
|
srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1));
|
||||||
|
edgeColor = data.edgeB[srcRow];
|
||||||
|
}
|
||||||
|
const int gray = computeGray(edgeColor, t);
|
||||||
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void SleepActivity::onEnter() {
|
void SleepActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
GUI.drawPopup(renderer, "Entering Sleep...");
|
GUI.drawPopup(renderer, "Entering Sleep...");
|
||||||
@@ -121,7 +483,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
|
|||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath) const {
|
||||||
int x, y;
|
int x, y;
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
@@ -129,45 +491,79 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
|
|
||||||
Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
||||||
pageWidth, pageHeight);
|
pageWidth, pageHeight);
|
||||||
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
|
||||||
// image will scale, make sure placement is right
|
|
||||||
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
|
||||||
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), ratio, screenRatio);
|
// Always compute aspect-ratio-preserving scale and position (supports both larger and smaller images)
|
||||||
if (ratio > screenRatio) {
|
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
// image wider than viewport ratio, scaled down image needs to be centered vertically
|
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
||||||
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
|
||||||
cropX = 1.0f - (screenRatio / ratio);
|
Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), ratio, screenRatio);
|
||||||
Serial.printf("[%lu] [SLP] Cropping bitmap x: %f\n", millis(), cropX);
|
if (ratio > screenRatio) {
|
||||||
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
// image wider than viewport ratio, needs to be centered vertically
|
||||||
}
|
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||||
x = 0;
|
cropX = 1.0f - (screenRatio / ratio);
|
||||||
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
Serial.printf("[%lu] [SLP] Cropping bitmap x: %f\n", millis(), cropX);
|
||||||
Serial.printf("[%lu] [SLP] Centering with ratio %f to y=%d\n", millis(), ratio, y);
|
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
} else {
|
|
||||||
// image taller than viewport ratio, scaled down image needs to be centered horizontally
|
|
||||||
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
|
||||||
cropY = 1.0f - (ratio / screenRatio);
|
|
||||||
Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY);
|
|
||||||
ratio = static_cast<float>(bitmap.getWidth()) / ((1.0f - cropY) * static_cast<float>(bitmap.getHeight()));
|
|
||||||
}
|
|
||||||
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
|
|
||||||
y = 0;
|
|
||||||
Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x);
|
|
||||||
}
|
}
|
||||||
|
x = 0;
|
||||||
|
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
||||||
|
Serial.printf("[%lu] [SLP] Centering with ratio %f to y=%d\n", millis(), ratio, y);
|
||||||
} else {
|
} else {
|
||||||
// center the image
|
// image taller than or equal to viewport ratio, needs to be centered horizontally
|
||||||
x = (pageWidth - bitmap.getWidth()) / 2;
|
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||||
y = (pageHeight - bitmap.getHeight()) / 2;
|
cropY = 1.0f - (ratio / screenRatio);
|
||||||
|
Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY);
|
||||||
|
ratio = static_cast<float>(bitmap.getWidth()) / ((1.0f - cropY) * static_cast<float>(bitmap.getHeight()));
|
||||||
|
}
|
||||||
|
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
|
||||||
|
y = 0;
|
||||||
|
Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
|
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
|
||||||
|
|
||||||
|
// Compute the scale factor (same formula as drawBitmap) so we can map screen coords to source coords
|
||||||
|
const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth();
|
||||||
|
const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight();
|
||||||
|
const float scale =
|
||||||
|
std::min(static_cast<float>(pageWidth) / effectiveWidth, static_cast<float>(pageHeight) / effectiveHeight);
|
||||||
|
|
||||||
|
// Determine letterbox fill settings
|
||||||
|
const uint8_t fillMode = SETTINGS.sleepScreenLetterboxFill;
|
||||||
|
const bool wantFill = (fillMode != CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_NONE);
|
||||||
|
const int targetColor =
|
||||||
|
(SETTINGS.sleepScreenGradientDir == CrossPointSettings::SLEEP_SCREEN_GRADIENT_DIR::GRADIENT_TO_BLACK) ? 0 : 255;
|
||||||
|
|
||||||
|
static const char* fillModeNames[] = {"none", "solid", "blended", "gradient"};
|
||||||
|
const char* fillModeName = (fillMode < 4) ? fillModeNames[fillMode] : "unknown";
|
||||||
|
|
||||||
|
// Load cached edge data or sample from bitmap (first pass over bitmap, then rewind)
|
||||||
|
LetterboxGradientData gradientData;
|
||||||
|
const bool hasLetterbox = (x > 0 || y > 0);
|
||||||
|
if (hasLetterbox && wantFill) {
|
||||||
|
bool cacheLoaded = false;
|
||||||
|
if (!edgeCachePath.empty()) {
|
||||||
|
cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData);
|
||||||
|
}
|
||||||
|
if (!cacheLoaded) {
|
||||||
|
Serial.printf("[%lu] [SLP] Letterbox detected (x=%d, y=%d), sampling edges for %s fill\n", millis(), x, y,
|
||||||
|
fillModeName);
|
||||||
|
gradientData = sampleBitmapEdges(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY);
|
||||||
|
if (!edgeCachePath.empty() && gradientData.edgeA) {
|
||||||
|
saveEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
||||||
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
|
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
|
||||||
|
|
||||||
|
// Draw letterbox fill (BW pass)
|
||||||
|
if (gradientData.edgeA) {
|
||||||
|
drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor);
|
||||||
|
}
|
||||||
|
|
||||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||||
|
|
||||||
if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) {
|
if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) {
|
||||||
@@ -180,18 +576,26 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
bitmap.rewindToData();
|
bitmap.rewindToData();
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
|
if (gradientData.edgeA) {
|
||||||
|
drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor);
|
||||||
|
}
|
||||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
renderer.copyGrayscaleLsbBuffers();
|
||||||
|
|
||||||
bitmap.rewindToData();
|
bitmap.rewindToData();
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||||
|
if (gradientData.edgeA) {
|
||||||
|
drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor);
|
||||||
|
}
|
||||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||||
renderer.copyGrayscaleMsbBuffers();
|
renderer.copyGrayscaleMsbBuffers();
|
||||||
|
|
||||||
renderer.displayGrayBuffer();
|
renderer.displayGrayBuffer();
|
||||||
renderer.setRenderMode(GfxRenderer::BW);
|
renderer.setRenderMode(GfxRenderer::BW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gradientData.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SleepActivity::renderCoverSleepScreen() const {
|
void SleepActivity::renderCoverSleepScreen() const {
|
||||||
@@ -261,12 +665,18 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin)
|
||||||
|
std::string edgeCachePath;
|
||||||
|
if (coverBmpPath.size() > 4) {
|
||||||
|
edgeCachePath = coverBmpPath.substr(0, coverBmpPath.size() - 4) + "_edges.bin";
|
||||||
|
}
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath.c_str());
|
Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath.c_str());
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap, edgeCachePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
|
||||||
class Bitmap;
|
class Bitmap;
|
||||||
@@ -13,6 +15,6 @@ class SleepActivity final : public Activity {
|
|||||||
void renderDefaultSleepScreen() const;
|
void renderDefaultSleepScreen() const;
|
||||||
void renderCustomSleepScreen() const;
|
void renderCustomSleepScreen() const;
|
||||||
void renderCoverSleepScreen() const;
|
void renderCoverSleepScreen() const;
|
||||||
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
|
void renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath = "") const;
|
||||||
void renderBlankSleepScreen() const;
|
void renderBlankSleepScreen() const;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user