feat(home): Improve Continue Reading cover with 1-bit Atkinson dithering

- Add 1-bit BMP generation with Atkinson dithering for better quality thumbnails
- Replace noise dithering with error diffusion for smoother gradients
- Add fillPolygon() to GfxRenderer for proper bookmark ribbon shape
- Change bookmark from rect+triangle carve to pentagon polygon
- Fix bookmark inversion when Continue Reading card is selected
- Show selection state on first render (not just after navigation)
- Fix 1-bit BMP palette lookup in Bitmap::readRow()
- Add drawBitmap1Bit() optimized path for 1-bit BMPs
The 1-bit format eliminates gray passes on home screen for faster rendering
while Atkinson dithering maintains good image quality through error diffusion.
This commit is contained in:
Eunchurn Park
2026-01-05 00:47:42 +09:00
parent 6fbdd06101
commit fbda7aa4f1
9 changed files with 433 additions and 105 deletions

View File

@@ -211,12 +211,18 @@ void HomeActivity::render() {
constexpr int bookY = 30;
const bool bookSelected = hasContinueReading && selectorIndex == 0;
// Bookmark dimensions (used in multiple places)
const int bookmarkWidth = bookWidth / 8;
const int bookmarkHeight = bookHeight / 5;
const int bookmarkX = bookX + bookWidth - bookmarkWidth - 10;
const int bookmarkY = bookY + 5;
// Draw book card regardless, fill with message based on `hasContinueReading`
{
// Draw cover image as background if available (inside the box)
// Only load from SD on first render, then use stored buffer
if (hasContinueReading && hasCoverImage && !coverBmpPath.empty() && !coverRendered) {
// First time: load cover from SD and store buffer
// First time: load cover from SD and render
FsFile file;
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file);
@@ -246,9 +252,39 @@ void HomeActivity::render() {
// Draw border around the card
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
coverRendered = true;
// Store the buffer with cover image for fast navigation
// Draw bookmark ribbon immediately after cover
const int notchDepth = bookmarkHeight / 3;
const int centerX = bookmarkX + bookmarkWidth / 2;
const int xPoints[5] = {
bookmarkX, // top-left
bookmarkX + bookmarkWidth, // top-right
bookmarkX + bookmarkWidth, // bottom-right
centerX, // center notch point
bookmarkX // bottom-left
};
const int yPoints[5] = {
bookmarkY, // top-left
bookmarkY, // top-right
bookmarkY + bookmarkHeight, // bottom-right
bookmarkY + bookmarkHeight - notchDepth, // center notch point
bookmarkY + bookmarkHeight // bottom-left
};
// Draw bookmark ribbon (white normally, will be inverted if selected)
renderer.fillPolygon(xPoints, yPoints, 5, false);
// Store the buffer with cover image AND bookmark for fast navigation
coverBufferStored = renderer.storeBwBuffer();
coverRendered = true;
// First render: if selected, draw selection indicators now
if (bookSelected) {
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
// Invert bookmark to black
renderer.fillPolygon(xPoints, yPoints, 5, true);
}
}
file.close();
}
@@ -261,10 +297,33 @@ void HomeActivity::render() {
}
}
// If buffer was restored, just draw selection border if needed
if (bufferRestored && bookSelected) {
// If buffer was restored, draw selection indicators if needed
if (bufferRestored && bookSelected && coverRendered) {
// Draw selection border
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
// Invert bookmark color when selected (draw black over the white bookmark)
const int notchDepth = bookmarkHeight / 3;
const int centerX = bookmarkX + bookmarkWidth / 2;
const int xPoints[5] = {
bookmarkX, // top-left
bookmarkX + bookmarkWidth, // top-right
bookmarkX + bookmarkWidth, // bottom-right
centerX, // center notch point
bookmarkX // bottom-left
};
const int yPoints[5] = {
bookmarkY, // top-left
bookmarkY, // top-right
bookmarkY + bookmarkHeight, // bottom-right
bookmarkY + bookmarkHeight - notchDepth, // center notch point
bookmarkY + bookmarkHeight // bottom-left
};
// Draw black filled bookmark ribbon (inverted)
renderer.fillPolygon(xPoints, yPoints, 5, true);
} else if (!coverRendered) {
// No cover: draw border for non-cover case
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
@@ -273,33 +332,6 @@ void HomeActivity::render() {
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
}
}
// Bookmark icon in the top-right corner of the card (inside the box)
// Skip if buffer was restored (bookmark is already in the buffer)
if (!bufferRestored) {
const int bookmarkWidth = bookWidth / 8;
const int bookmarkHeight = bookHeight / 5;
const int bookmarkX = bookX + bookWidth - bookmarkWidth - 10;
const int bookmarkY = bookY + 5;
// Main bookmark body (solid) - white on cover, inverted on selection
const bool bookmarkWhite = coverRendered ? true : !bookSelected;
renderer.fillRect(bookmarkX, bookmarkY, bookmarkWidth, bookmarkHeight, bookmarkWhite);
// Carve out an inverted triangle notch at the bottom center to create angled points
const int notchHeight = bookmarkHeight / 2; // depth of the notch
const bool notchColor = coverRendered ? false : bookSelected;
for (int i = 0; i < notchHeight; ++i) {
const int y = bookmarkY + bookmarkHeight - 1 - i;
const int xStart = bookmarkX + i;
const int width = bookmarkWidth - 2 * i;
if (width <= 0) {
break;
}
// Draw a horizontal strip in the opposite color to "cut" the notch
renderer.fillRect(xStart, y, width, 1, notchColor);
}
}
}
if (hasContinueReading) {