Merge pull request #43 from jpirnay/feat-darkness
feat: Add darkness levels for antialiasing
This commit is contained in:
+147
-61
@@ -382,29 +382,105 @@ static inline uint8_t get2BitPixel(const uint8_t* const bitmap, const int stride
|
|||||||
return get2BitPixel(bitmap, row * stride + col);
|
return get2BitPixel(bitmap, row * stride + col);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <GfxRenderer::RenderMode mode>
|
// Compute the runtime drawMask for a given render mode and text darkness.
|
||||||
static constexpr uint8_t drawMaskFor2BitMode() {
|
// Bit N set ⇒ draw when raw 2-bit font value == N
|
||||||
if constexpr (mode == GfxRenderer::BW)
|
// (raw: 0=white, 1=light gray, 2=dark gray, 3=black).
|
||||||
return 0x0E; // draw raw {1,2,3}
|
//
|
||||||
else if constexpr (mode == GfxRenderer::GRAYSCALE_MSB)
|
// BW always draws every non-white pixel (darkness has no effect).
|
||||||
return 0x06; // draw raw {1,2}
|
// For grayscale modes, increasing darkness folds more AA shades into the
|
||||||
else
|
// "draw" set so text becomes progressively bolder. The default darkness=1
|
||||||
return 0x04; // GRAYSCALE_LSB: draw raw {2}
|
// keeps the historical behavior (MSB pass draws both AA shades, LSB pass
|
||||||
|
// draws only the dark AA shade).
|
||||||
|
//
|
||||||
|
// At "Maximum" (darkness>=3) the grayscale passes are suppressed entirely
|
||||||
|
// (drawMask 0x00). The BW pass already writes raw {1,2,3} as solid black,
|
||||||
|
// so AA pixels render as hard black with no gray-LUT softening — visibly
|
||||||
|
// darker than darkness=2 because the gray waveform is skipped.
|
||||||
|
//
|
||||||
|
// darkness | GRAYSCALE_MSB | GRAYSCALE_LSB
|
||||||
|
// --------- ------------------------- -------------------------
|
||||||
|
// 0 | 0x02 (raw {1}) | 0x04 (raw {2})
|
||||||
|
// 1 | 0x06 (raw {1,2}) ←dflt | 0x04 (raw {2}) ←dflt
|
||||||
|
// 2 | 0x06 (raw {1,2}) | 0x06 (raw {1,2})
|
||||||
|
// 3+ | 0x00 (none) | 0x00 (none)
|
||||||
|
//
|
||||||
|
// ─── Worked example ────────────────────────────────────────────────────────
|
||||||
|
// Imagine a 2-bit antialiased glyph for the diagonal stroke of a letter 'A'.
|
||||||
|
// Each cell holds the raw font value at that pixel:
|
||||||
|
//
|
||||||
|
// raw values . . . 2 3 legend:
|
||||||
|
// . . 2 3 1 . = 0 (white, never drawn)
|
||||||
|
// . 2 3 1 . 1 = light gray AA
|
||||||
|
// 2 3 1 . . 2 = dark gray AA
|
||||||
|
// 3 1 . . . 3 = solid black (stroke core)
|
||||||
|
//
|
||||||
|
// Three render passes write to three independent planes; the panel's
|
||||||
|
// grayscale waveform combines the BW plane with (MSB,LSB) into 4 shades:
|
||||||
|
//
|
||||||
|
// (MSB, LSB) → panel shade
|
||||||
|
// (0,0) → white
|
||||||
|
// (1,0) → light gray
|
||||||
|
// (0,1) → dark gray
|
||||||
|
// (1,1) → black
|
||||||
|
//
|
||||||
|
// Per-pixel result for each darkness level (●=black, ▓=dark gray,
|
||||||
|
// ░=light gray, ·=white):
|
||||||
|
//
|
||||||
|
// darkness=0 Normal — true 4-level AA
|
||||||
|
// . . . ▓ ● raw=1 → (1,0) light gray
|
||||||
|
// . . ▓ ● ░ raw=2 → (0,1) dark gray
|
||||||
|
// . ▓ ● ░ . raw=3 → BW black
|
||||||
|
// ▓ ● ░ . . Crisp edges, lightest stroke. Best for thin/serif fonts.
|
||||||
|
// ● ░ . . .
|
||||||
|
//
|
||||||
|
// darkness=1 Dark — historical default
|
||||||
|
// . . . ● ● raw=1 → (1,0) light gray (unchanged)
|
||||||
|
// . . ● ● ░ raw=2 → (1,1) black (was dark gray)
|
||||||
|
// . ● ● ░ . Dark-gray fringe collapses to black; light fringe
|
||||||
|
// ● ● ░ . . survives. Stroke core thickens by ~1px on the
|
||||||
|
// ● ░ . . . steep side of the slope.
|
||||||
|
//
|
||||||
|
// darkness=2 Extra Dark — both AA shades go black
|
||||||
|
// . . . ● ● raw=1 → (1,1) black
|
||||||
|
// . . ● ● ● raw=2 → (1,1) black
|
||||||
|
// . ● ● ● . All AA pixels are pushed to "black" in the gray
|
||||||
|
// ● ● ● . . plane. The gray waveform still runs, so pixels
|
||||||
|
// ● ● . . . share the gray-pass voltage profile (slightly
|
||||||
|
// softer than Maximum).
|
||||||
|
//
|
||||||
|
// darkness=3 Maximum — grayscale pass skipped entirely
|
||||||
|
// . . . ● ● Both grayscale drawMasks are 0x00; nothing is
|
||||||
|
// . . ● ● ● written to the (MSB,LSB) planes. The BW pass —
|
||||||
|
// . ● ● ● . which already writes raw {1,2,3} as solid black —
|
||||||
|
// ● ● ● . . is the only pass the panel sees, refreshed with
|
||||||
|
// ● ● . . . the hard FAST waveform. Visually identical pixel
|
||||||
|
// footprint to darkness=2 but driven harder, so
|
||||||
|
// strokes look noticeably bolder/blacker on the
|
||||||
|
// physical e-ink panel.
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
static inline uint8_t drawMaskFor2BitMode(const GfxRenderer::RenderMode mode, const uint8_t darkness) {
|
||||||
|
if (mode == GfxRenderer::BW) return 0x0E; // draw raw {1,2,3}
|
||||||
|
if (darkness >= 3) return 0x00; // skip grayscale entirely (Maximum)
|
||||||
|
if (mode == GfxRenderer::GRAYSCALE_MSB) {
|
||||||
|
return (darkness == 0) ? 0x02 : 0x06;
|
||||||
|
}
|
||||||
|
// GRAYSCALE_LSB
|
||||||
|
return (darkness >= 2) ? 0x06 : 0x04;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2-bit pipeline — fused gather+threshold (X axis): the 2-bit analog of extractGlyphBlock, but
|
// 2-bit pipeline — fused gather+threshold (X axis): the 2-bit analog of extractGlyphBlock, but
|
||||||
// gather and threshold are collapsed into one pass. The threshold (2-bit raw value → 1-bit on/off)
|
// gather and threshold are collapsed into one pass. The threshold (2-bit raw value → 1-bit on/off)
|
||||||
// is information-lossy, so no contiguous 2-bit intermediate block can be formed mid-pipeline.
|
// is information-lossy, so no contiguous 2-bit intermediate block can be formed mid-pipeline.
|
||||||
// The resulting 1-bit mask feeds writeRowBits directly (scatter). build2BitColMask is the Y-axis counterpart.
|
// The resulting 1-bit mask feeds writeRowBits directly (scatter). build2BitColMask is the Y-axis counterpart.
|
||||||
template <GfxRenderer::RenderMode mode>
|
//
|
||||||
|
// Templated on the drawMask byte (a non-type template parameter) so each render-mode/darkness
|
||||||
|
// combination compiles to its own specialization with the mask folded into a constant.
|
||||||
|
template <uint8_t drawMask>
|
||||||
static inline uint8_t build2BitRowMask(const uint8_t* const bitmap, const int rowStartPixel, const int glyphXStartOrEnd,
|
static inline uint8_t build2BitRowMask(const uint8_t* const bitmap, const int rowStartPixel, const int glyphXStartOrEnd,
|
||||||
const int count, const bool reverseXInChunk) {
|
const int count, const bool reverseXInChunk) {
|
||||||
// drawMask uses raw 2-bit glyph values directly from font bitmaps:
|
// drawMask uses raw 2-bit glyph values directly from font bitmaps:
|
||||||
// raw 0=white, 1=light gray, 2=dark gray, 3=black.
|
// raw 0=white, 1=light gray, 2=dark gray, 3=black.
|
||||||
// Bit N set means: draw/update when raw==N.
|
// Bit N set means: draw/update when raw==N.
|
||||||
// Compile-time constant lets the compiler reduce (drawMask >> raw) & 1 to a single comparison.
|
|
||||||
constexpr uint8_t drawMask = drawMaskFor2BitMode<mode>();
|
|
||||||
|
|
||||||
uint8_t mask = 0;
|
uint8_t mask = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
const int logicalX = reverseXInChunk ? (glyphXStartOrEnd - i) : (glyphXStartOrEnd + i);
|
const int logicalX = reverseXInChunk ? (glyphXStartOrEnd - i) : (glyphXStartOrEnd + i);
|
||||||
@@ -419,10 +495,11 @@ static inline uint8_t build2BitRowMask(const uint8_t* const bitmap, const int ro
|
|||||||
// The 2-bit glyph bitmap stores 4 pixels per byte, MSB-first:
|
// The 2-bit glyph bitmap stores 4 pixels per byte, MSB-first:
|
||||||
// byte b = [p0.msb p0.lsb p1.msb p1.lsb p2.msb p2.lsb p3.msb p3.lsb]
|
// byte b = [p0.msb p0.lsb p1.msb p1.lsb p2.msb p2.lsb p3.msb p3.lsb]
|
||||||
//
|
//
|
||||||
// For each render mode the draw decision collapses to a two-bit boolean:
|
// For each drawMask the draw decision collapses to a two-bit boolean:
|
||||||
// BW (draw if raw ≠ 0): msb | lsb
|
// 0x0E (raw ∈ {1,2,3}): msb | lsb
|
||||||
// GRAYSCALE_MSB (draw if raw ∈ {1,2}): msb ^ lsb
|
// 0x06 (raw ∈ {1,2}): msb ^ lsb
|
||||||
// GRAYSCALE_LSB (draw if raw == 2): msb & ~lsb
|
// 0x04 (raw == 2): msb & ~lsb
|
||||||
|
// 0x02 (raw == 1): ~msb & lsb
|
||||||
//
|
//
|
||||||
// Derivation for one byte:
|
// Derivation for one byte:
|
||||||
// msb_bits = b & 0xAA → bits 7,5,3,1 hold p0.msb … p3.msb; bits 6,4,2,0 = 0
|
// msb_bits = b & 0xAA → bits 7,5,3,1 hold p0.msb … p3.msb; bits 6,4,2,0 = 0
|
||||||
@@ -439,7 +516,7 @@ static inline uint8_t build2BitRowMask(const uint8_t* const bitmap, const int ro
|
|||||||
// processes the full 8-pixel chunk in ~16 ALU ops instead of ~56.
|
// processes the full 8-pixel chunk in ~16 ALU ops instead of ~56.
|
||||||
// The caller is responsible for only calling this when pixelStart is
|
// The caller is responsible for only calling this when pixelStart is
|
||||||
// 4-pixel (1-byte) aligned (pixelStart & 3 == 0) and count == 8.
|
// 4-pixel (1-byte) aligned (pixelStart & 3 == 0) and count == 8.
|
||||||
template <GfxRenderer::RenderMode mode>
|
template <uint8_t drawMask>
|
||||||
static inline uint8_t build2BitRowMaskFromTwoBytes(const uint8_t b0, const uint8_t b1) {
|
static inline uint8_t build2BitRowMaskFromTwoBytes(const uint8_t b0, const uint8_t b1) {
|
||||||
const uint8_t msb0 = b0 & 0xAA;
|
const uint8_t msb0 = b0 & 0xAA;
|
||||||
const uint8_t lsb0 = (b0 & 0x55) << 1;
|
const uint8_t lsb0 = (b0 & 0x55) << 1;
|
||||||
@@ -447,15 +524,19 @@ static inline uint8_t build2BitRowMaskFromTwoBytes(const uint8_t b0, const uint8
|
|||||||
const uint8_t lsb1 = (b1 & 0x55) << 1;
|
const uint8_t lsb1 = (b1 & 0x55) << 1;
|
||||||
|
|
||||||
uint8_t draw0, draw1;
|
uint8_t draw0, draw1;
|
||||||
if constexpr (mode == GfxRenderer::BW) {
|
if constexpr (drawMask == 0x0E) { // BW: raw ∈ {1,2,3}
|
||||||
draw0 = msb0 | lsb0;
|
draw0 = msb0 | lsb0;
|
||||||
draw1 = msb1 | lsb1;
|
draw1 = msb1 | lsb1;
|
||||||
} else if constexpr (mode == GfxRenderer::GRAYSCALE_MSB) {
|
} else if constexpr (drawMask == 0x06) { // raw ∈ {1,2}
|
||||||
draw0 = msb0 ^ lsb0;
|
draw0 = msb0 ^ lsb0;
|
||||||
draw1 = msb1 ^ lsb1;
|
draw1 = msb1 ^ lsb1;
|
||||||
} else { // GRAYSCALE_LSB
|
} else if constexpr (drawMask == 0x04) { // raw == 2 (dark gray)
|
||||||
draw0 = msb0 & ~lsb0;
|
draw0 = msb0 & ~lsb0;
|
||||||
draw1 = msb1 & ~lsb1;
|
draw1 = msb1 & ~lsb1;
|
||||||
|
} else { // drawMask == 0x02, raw == 1 (light gray)
|
||||||
|
static_assert(drawMask == 0x02, "unsupported drawMask in build2BitRowMaskFromTwoBytes");
|
||||||
|
draw0 = ~msb0 & lsb0;
|
||||||
|
draw1 = ~msb1 & lsb1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact each nibble's draw flags from bit positions 7,5,3,1 → 7,6,5,4.
|
// Compact each nibble's draw flags from bit positions 7,5,3,1 → 7,6,5,4.
|
||||||
@@ -468,10 +549,9 @@ static inline uint8_t build2BitRowMaskFromTwoBytes(const uint8_t b0, const uint8
|
|||||||
// 2-bit pipeline — fused gather+threshold (Y axis): column-direction counterpart to build2BitRowMask.
|
// 2-bit pipeline — fused gather+threshold (Y axis): column-direction counterpart to build2BitRowMask.
|
||||||
// Samples count pixels down glyph column glyphX starting at row glyphYStart; reverseRows implements
|
// Samples count pixels down glyph column glyphX starting at row glyphYStart; reverseRows implements
|
||||||
// a negative-stride view along Y (reads bottom-to-top), needed for PortraitInverted.
|
// a negative-stride view along Y (reads bottom-to-top), needed for PortraitInverted.
|
||||||
template <GfxRenderer::RenderMode mode>
|
template <uint8_t drawMask>
|
||||||
static inline uint8_t build2BitColMask(const uint8_t* const bitmap, const int glyphWidth, const int glyphX,
|
static inline uint8_t build2BitColMask(const uint8_t* const bitmap, const int glyphWidth, const int glyphX,
|
||||||
const int glyphYStart, const int count, const bool reverseRows) {
|
const int glyphYStart, const int count, const bool reverseRows) {
|
||||||
constexpr uint8_t drawMask = drawMaskFor2BitMode<mode>();
|
|
||||||
uint8_t mask = 0;
|
uint8_t mask = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
const int row = reverseRows ? (glyphYStart + count - 1 - i) : (glyphYStart + i);
|
const int row = reverseRows ? (glyphYStart + count - 1 - i) : (glyphYStart + i);
|
||||||
@@ -485,7 +565,7 @@ static inline uint8_t build2BitColMask(const uint8_t* const bitmap, const int gl
|
|||||||
// inverted=false → Portrait (phyY counts down, phyBitPos counts up).
|
// inverted=false → Portrait (phyY counts down, phyBitPos counts up).
|
||||||
// inverted=true → PortraitInverted (phyY counts up, phyBitPos counts down).
|
// inverted=true → PortraitInverted (phyY counts up, phyBitPos counts down).
|
||||||
// Both template params are compile-time constants; all ternaries fold away.
|
// Both template params are compile-time constants; all ternaries fold away.
|
||||||
template <GfxRenderer::RenderMode mode, bool inverted>
|
template <uint8_t drawMask, bool inverted>
|
||||||
static void renderGlyphFast2BitPortrait(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
static void renderGlyphFast2BitPortrait(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
||||||
const int glyphHeight, const int screenXBase, const int screenYBase,
|
const int glyphHeight, const int screenXBase, const int screenYBase,
|
||||||
const bool writeState) {
|
const bool writeState) {
|
||||||
@@ -495,7 +575,7 @@ static void renderGlyphFast2BitPortrait(uint8_t* const frameBuffer, const uint8_
|
|||||||
uint8_t* const row = frameBuffer + phyY * HalDisplay::DISPLAY_WIDTH_BYTES;
|
uint8_t* const row = frameBuffer + phyY * HalDisplay::DISPLAY_WIDTH_BYTES;
|
||||||
for (int glyphY = 0; glyphY < glyphHeight; glyphY += 8) {
|
for (int glyphY = 0; glyphY < glyphHeight; glyphY += 8) {
|
||||||
const int count = std::min(8, glyphHeight - glyphY);
|
const int count = std::min(8, glyphHeight - glyphY);
|
||||||
const uint8_t mask = build2BitColMask<mode>(bitmap, glyphWidth, glyphX, glyphY, count, inverted);
|
const uint8_t mask = build2BitColMask<drawMask>(bitmap, glyphWidth, glyphX, glyphY, count, inverted);
|
||||||
if (mask == 0) continue;
|
if (mask == 0) continue;
|
||||||
const int phyBitPos =
|
const int phyBitPos =
|
||||||
inverted ? (HalDisplay::DISPLAY_WIDTH - 1 - screenYBase - (glyphY + count - 1)) : (screenYBase + glyphY);
|
inverted ? (HalDisplay::DISPLAY_WIDTH - 1 - screenYBase - (glyphY + count - 1)) : (screenYBase + glyphY);
|
||||||
@@ -505,13 +585,14 @@ static void renderGlyphFast2BitPortrait(uint8_t* const frameBuffer, const uint8_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
template <GfxRenderer::RenderMode mode>
|
template <uint8_t drawMask>
|
||||||
static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const bitmap, const int glyphWidth,
|
||||||
const int glyphHeight, const int screenXBase, const int screenYBase,
|
const int glyphHeight, const int screenXBase, const int screenYBase,
|
||||||
const bool pixelState, const GfxRenderer::Orientation orientation) {
|
const bool pixelState, const GfxRenderer::Orientation orientation) {
|
||||||
// Non-rotated text fast path for 2-bit glyphs. Writes compact masks directly to framebuffer rows.
|
// Non-rotated text fast path for 2-bit glyphs. Writes compact masks directly to framebuffer rows.
|
||||||
// TextRotation::Rotated90CW keeps the legacy per-pixel fallback path for safety and readability.
|
// TextRotation::Rotated90CW keeps the legacy per-pixel fallback path for safety and readability.
|
||||||
const bool writeState = (mode == GfxRenderer::BW) ? pixelState : false;
|
// BW (drawMask 0x0E) honors the caller's pixelState; grayscale passes always clear the bit.
|
||||||
|
const bool writeState = (drawMask == 0x0E) ? pixelState : false;
|
||||||
|
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case GfxRenderer::LandscapeCounterClockwise: {
|
case GfxRenderer::LandscapeCounterClockwise: {
|
||||||
@@ -526,9 +607,9 @@ static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const
|
|||||||
uint8_t mask;
|
uint8_t mask;
|
||||||
if (count == 8 && (pixelStart & 3) == 0) {
|
if (count == 8 && (pixelStart & 3) == 0) {
|
||||||
const int srcByteIdx = pixelStart >> 2;
|
const int srcByteIdx = pixelStart >> 2;
|
||||||
mask = build2BitRowMaskFromTwoBytes<mode>(bitmap[srcByteIdx], bitmap[srcByteIdx + 1]);
|
mask = build2BitRowMaskFromTwoBytes<drawMask>(bitmap[srcByteIdx], bitmap[srcByteIdx + 1]);
|
||||||
} else {
|
} else {
|
||||||
mask = build2BitRowMask<mode>(bitmap, rowStartPixel, glyphX, count, false);
|
mask = build2BitRowMask<drawMask>(bitmap, rowStartPixel, glyphX, count, false);
|
||||||
}
|
}
|
||||||
if (mask == 0) continue;
|
if (mask == 0) continue;
|
||||||
const int phyBitPos = screenXBase + glyphX;
|
const int phyBitPos = screenXBase + glyphX;
|
||||||
@@ -555,9 +636,9 @@ static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const
|
|||||||
uint8_t mask;
|
uint8_t mask;
|
||||||
if (count == 8 && (pixelStart & 3) == 0) {
|
if (count == 8 && (pixelStart & 3) == 0) {
|
||||||
const int srcByteIdx = pixelStart >> 2;
|
const int srcByteIdx = pixelStart >> 2;
|
||||||
mask = reverseBits8(build2BitRowMaskFromTwoBytes<mode>(bitmap[srcByteIdx], bitmap[srcByteIdx + 1]));
|
mask = reverseBits8(build2BitRowMaskFromTwoBytes<drawMask>(bitmap[srcByteIdx], bitmap[srcByteIdx + 1]));
|
||||||
} else {
|
} else {
|
||||||
mask = build2BitRowMask<mode>(bitmap, rowStartPixel, chunkEnd, count, true);
|
mask = build2BitRowMask<drawMask>(bitmap, rowStartPixel, chunkEnd, count, true);
|
||||||
}
|
}
|
||||||
if (mask == 0) continue;
|
if (mask == 0) continue;
|
||||||
const int phyBitPos = HalDisplay::DISPLAY_WIDTH - 1 - screenXBase - chunkEnd;
|
const int phyBitPos = HalDisplay::DISPLAY_WIDTH - 1 - screenXBase - chunkEnd;
|
||||||
@@ -569,13 +650,13 @@ static void renderGlyphFast2Bit(uint8_t* const frameBuffer, const uint8_t* const
|
|||||||
}
|
}
|
||||||
|
|
||||||
case GfxRenderer::Portrait:
|
case GfxRenderer::Portrait:
|
||||||
renderGlyphFast2BitPortrait<mode, false>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase, screenYBase,
|
renderGlyphFast2BitPortrait<drawMask, false>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
||||||
writeState);
|
screenYBase, writeState);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GfxRenderer::PortraitInverted:
|
case GfxRenderer::PortraitInverted:
|
||||||
renderGlyphFast2BitPortrait<mode, true>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase, screenYBase,
|
renderGlyphFast2BitPortrait<drawMask, true>(frameBuffer, bitmap, glyphWidth, glyphHeight, screenXBase,
|
||||||
writeState);
|
screenYBase, writeState);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -614,27 +695,42 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is2Bit) {
|
if (is2Bit) {
|
||||||
|
// Compute the drawMask once per glyph from the current render mode + text-darkness setting.
|
||||||
|
// The fast path dispatches on this at runtime to a template specialization so the mask is
|
||||||
|
// a compile-time constant inside the inner loops.
|
||||||
|
const uint8_t drawMask = drawMaskFor2BitMode(renderMode, renderer.getTextDarkness());
|
||||||
|
|
||||||
|
// drawMask == 0 means "draw nothing" — used by Maximum darkness to skip grayscale passes.
|
||||||
|
if (drawMask == 0) return;
|
||||||
|
|
||||||
if constexpr (rotation == TextRotation::None) {
|
if constexpr (rotation == TextRotation::None) {
|
||||||
// Fast path for normal text orientation. Handles all device orientations via renderGlyphFast2Bit.
|
// Fast path for normal text orientation. Handles all device orientations via renderGlyphFast2Bit.
|
||||||
// Dispatch on renderMode at compile time so each specialization gets a constant drawMask.
|
switch (drawMask) {
|
||||||
switch (renderMode) {
|
case 0x0E: // BW
|
||||||
case GfxRenderer::BW:
|
renderGlyphFast2Bit<0x0E>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||||
renderGlyphFast2Bit<GfxRenderer::BW>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
pixelState, renderer.getOrientation());
|
||||||
pixelState, renderer.getOrientation());
|
|
||||||
break;
|
break;
|
||||||
case GfxRenderer::GRAYSCALE_MSB:
|
case 0x06: // raw {1,2}
|
||||||
renderGlyphFast2Bit<GfxRenderer::GRAYSCALE_MSB>(renderer.getFrameBuffer(), bitmap, width, height, innerBase,
|
renderGlyphFast2Bit<0x06>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||||
outerBase, pixelState, renderer.getOrientation());
|
pixelState, renderer.getOrientation());
|
||||||
break;
|
break;
|
||||||
case GfxRenderer::GRAYSCALE_LSB:
|
case 0x04: // raw {2}
|
||||||
renderGlyphFast2Bit<GfxRenderer::GRAYSCALE_LSB>(renderer.getFrameBuffer(), bitmap, width, height, innerBase,
|
renderGlyphFast2Bit<0x04>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||||
outerBase, pixelState, renderer.getOrientation());
|
pixelState, renderer.getOrientation());
|
||||||
|
break;
|
||||||
|
case 0x02: // raw {1}
|
||||||
|
renderGlyphFast2Bit<0x02>(renderer.getFrameBuffer(), bitmap, width, height, innerBase, outerBase,
|
||||||
|
pixelState, renderer.getOrientation());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotated text fallback: keep explicit per-pixel behavior.
|
// Rotated text fallback: per-pixel path. Uses the same drawMask as the fast path so darkness
|
||||||
|
// takes effect uniformly. (Previously this branch had a separate X4-only "draw light gray too"
|
||||||
|
// quirk; that quirk is now subsumed by the default darkness=1 mask, which already includes
|
||||||
|
// both AA shades for the MSB pass.)
|
||||||
|
const bool isBW = (drawMask == 0x0E);
|
||||||
int pixelPosition = 0;
|
int pixelPosition = 0;
|
||||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||||
const int outerCoord = outerBase + glyphY;
|
const int outerCoord = outerBase + glyphY;
|
||||||
@@ -650,22 +746,12 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
|||||||
|
|
||||||
const uint8_t byte = bitmap[pixelPosition >> 2];
|
const uint8_t byte = bitmap[pixelPosition >> 2];
|
||||||
const uint8_t bit_index = (3 - (pixelPosition & 3)) * 2;
|
const uint8_t bit_index = (3 - (pixelPosition & 3)) * 2;
|
||||||
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
|
// raw value straight from the font: 0=white, 1=light gray, 2=dark gray, 3=black
|
||||||
// we swap this to better match the way images and screen think about colors:
|
const uint8_t raw = (byte >> bit_index) & 0x3;
|
||||||
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
|
|
||||||
const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3);
|
|
||||||
|
|
||||||
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
|
if ((drawMask >> raw) & 0x01) {
|
||||||
// Black (also paints over the grays in BW mode)
|
// BW honors caller's pixelState; grayscale passes always clear the bit (false)
|
||||||
renderer.drawPixel(screenX, screenY, pixelState);
|
renderer.drawPixel(screenX, screenY, isBW ? pixelState : false);
|
||||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || (gpio.deviceIsX4() && bmpVal == 2))) {
|
|
||||||
// Light gray (also mark the MSB if it's going to be a dark gray too)
|
|
||||||
// X3 AA tuning: keep only the darker antialias level to avoid washed text
|
|
||||||
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
|
||||||
renderer.drawPixel(screenX, screenY, false);
|
|
||||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) {
|
|
||||||
// Dark gray
|
|
||||||
renderer.drawPixel(screenX, screenY, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ class GfxRenderer {
|
|||||||
RenderMode renderMode;
|
RenderMode renderMode;
|
||||||
Orientation orientation;
|
Orientation orientation;
|
||||||
bool fadingFix;
|
bool fadingFix;
|
||||||
|
// Text darkness for 2-bit grayscale glyph rendering.
|
||||||
|
// 0 = Normal — true 4-level AA (raw=1 → light gray, raw=2 → dark gray)
|
||||||
|
// 1 = Dark — historical default; raw=2 collapses to black
|
||||||
|
// 2 = Extra Dark — both AA shades go black in the grayscale plane
|
||||||
|
// 3 = Maximum — grayscale pass skipped entirely; only the hard BW
|
||||||
|
// pass remains, so AA pixels render as solid black
|
||||||
|
// with the FAST waveform (no gray-LUT softening)
|
||||||
|
// Only affects AA pixels in GRAYSCALE_MSB / GRAYSCALE_LSB rendering of 2-bit fonts.
|
||||||
|
// 1-bit fonts and the BW pass are unchanged. Default is 1 to preserve historical
|
||||||
|
// rendering. See drawMaskFor2BitMode() in GfxRenderer.cpp for the per-level
|
||||||
|
// pixel breakdown and a worked example glyph.
|
||||||
|
uint8_t textDarkness = 1;
|
||||||
uint8_t* frameBuffer = nullptr;
|
uint8_t* frameBuffer = nullptr;
|
||||||
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
|
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
|
||||||
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
|
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
|
||||||
@@ -155,6 +167,14 @@ class GfxRenderer {
|
|||||||
// Grayscale functions
|
// Grayscale functions
|
||||||
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
||||||
RenderMode getRenderMode() const { return renderMode; }
|
RenderMode getRenderMode() const { return renderMode; }
|
||||||
|
|
||||||
|
// Text darkness control:
|
||||||
|
// 0 = Normal, 1 = Dark, 2 = Extra Dark, 3 = Maximum.
|
||||||
|
// Only affects anti-aliased pixels in 2-bit (grayscale) glyph rendering;
|
||||||
|
// 1-bit fonts and the BW pass are unchanged. See drawMaskFor2BitMode() in
|
||||||
|
// GfxRenderer.cpp for the per-level pixel breakdown and a worked example.
|
||||||
|
void setTextDarkness(const uint8_t d) { textDarkness = d; }
|
||||||
|
uint8_t getTextDarkness() const { return textDarkness; }
|
||||||
void copyGrayscaleLsbBuffers() const;
|
void copyGrayscaleLsbBuffers() const;
|
||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() const;
|
void displayGrayBuffer() const;
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
|
|||||||
STR_HIDE_BATTERY: "Hide Battery %"
|
STR_HIDE_BATTERY: "Hide Battery %"
|
||||||
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
||||||
STR_TEXT_AA: "Text Anti-Aliasing"
|
STR_TEXT_AA: "Text Anti-Aliasing"
|
||||||
|
STR_TEXT_DARKNESS: "Text Darkness"
|
||||||
|
STR_EXTRA_DARK: "Extra Dark"
|
||||||
|
STR_MAX_DARK: "Maximum"
|
||||||
STR_IMAGES: "Images"
|
STR_IMAGES: "Images"
|
||||||
STR_IMAGES_DISPLAY: "Display"
|
STR_IMAGES_DISPLAY: "Display"
|
||||||
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ STR_SLEEP_COVER_MODE: "Mode écran de veille"
|
|||||||
STR_HIDE_BATTERY: "Masquer % batterie"
|
STR_HIDE_BATTERY: "Masquer % batterie"
|
||||||
STR_EXTRA_SPACING: "Espacement paragraphes"
|
STR_EXTRA_SPACING: "Espacement paragraphes"
|
||||||
STR_TEXT_AA: "Lissage du texte"
|
STR_TEXT_AA: "Lissage du texte"
|
||||||
|
STR_TEXT_DARKNESS: "Densité du texte"
|
||||||
|
STR_EXTRA_DARK: "Très foncé"
|
||||||
|
STR_MAX_DARK: "Maximum"
|
||||||
STR_IMAGES: "Images"
|
STR_IMAGES: "Images"
|
||||||
STR_IMAGES_DISPLAY: "Affichage"
|
STR_IMAGES_DISPLAY: "Affichage"
|
||||||
STR_IMAGES_PLACEHOLDER: "Espace réservé"
|
STR_IMAGES_PLACEHOLDER: "Espace réservé"
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
|
|||||||
STR_HIDE_BATTERY: "Batterie % ausblenden"
|
STR_HIDE_BATTERY: "Batterie % ausblenden"
|
||||||
STR_EXTRA_SPACING: "Absatzabstand"
|
STR_EXTRA_SPACING: "Absatzabstand"
|
||||||
STR_TEXT_AA: "Schriftglättung"
|
STR_TEXT_AA: "Schriftglättung"
|
||||||
|
STR_TEXT_DARKNESS: "Schriftstärke"
|
||||||
|
STR_EXTRA_DARK: "Extra dunkel"
|
||||||
|
STR_MAX_DARK: "Maximum"
|
||||||
STR_IMAGE_DITHERING: "Bild-Dithering"
|
STR_IMAGE_DITHERING: "Bild-Dithering"
|
||||||
STR_IMAGE_DITHER_BAYER: "Bayer"
|
STR_IMAGE_DITHER_BAYER: "Bayer"
|
||||||
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
|
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ STR_SLEEP_COVER_MODE: "Copertina in sospensione"
|
|||||||
STR_HIDE_BATTERY: "Nascondi % batteria"
|
STR_HIDE_BATTERY: "Nascondi % batteria"
|
||||||
STR_EXTRA_SPACING: "Spaziatura extra paragrafi"
|
STR_EXTRA_SPACING: "Spaziatura extra paragrafi"
|
||||||
STR_TEXT_AA: "Anti-aliasing testo"
|
STR_TEXT_AA: "Anti-aliasing testo"
|
||||||
|
STR_TEXT_DARKNESS: "Intensità testo"
|
||||||
|
STR_EXTRA_DARK: "Molto scuro"
|
||||||
|
STR_MAX_DARK: "Massimo"
|
||||||
STR_IMAGES: "Immagini"
|
STR_IMAGES: "Immagini"
|
||||||
STR_IMAGES_DISPLAY: "Visualizza"
|
STR_IMAGES_DISPLAY: "Visualizza"
|
||||||
STR_IMAGES_PLACEHOLDER: "Segnaposto"
|
STR_IMAGES_PLACEHOLDER: "Segnaposto"
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ STR_SLEEP_COVER_MODE: "Modo de pantalla de suspensión"
|
|||||||
STR_HIDE_BATTERY: "Ocultar % de batería"
|
STR_HIDE_BATTERY: "Ocultar % de batería"
|
||||||
STR_EXTRA_SPACING: "Espaciado entre párrafos"
|
STR_EXTRA_SPACING: "Espaciado entre párrafos"
|
||||||
STR_TEXT_AA: "Suavizado de texto"
|
STR_TEXT_AA: "Suavizado de texto"
|
||||||
|
STR_TEXT_DARKNESS: "Intensidad del texto"
|
||||||
|
STR_EXTRA_DARK: "Muy oscuro"
|
||||||
|
STR_MAX_DARK: "Máximo"
|
||||||
STR_IMAGES: "Imágenes"
|
STR_IMAGES: "Imágenes"
|
||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
||||||
|
|||||||
@@ -138,6 +138,15 @@ class CrossPointSettings {
|
|||||||
|
|
||||||
// Image rendering in EPUB reader
|
// Image rendering in EPUB reader
|
||||||
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
|
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
|
||||||
|
|
||||||
|
// Text darkness for AA glyph rendering (forwarded to GfxRenderer::setTextDarkness)
|
||||||
|
enum TEXT_DARKNESS {
|
||||||
|
DARKNESS_NORMAL = 0, // true 4-level AA
|
||||||
|
DARKNESS_DARK = 1, // historical default — bolder dark-gray AA
|
||||||
|
DARKNESS_EXTRA_DARK = 2, // both AA shades pushed to darkest gray state
|
||||||
|
DARKNESS_MAXIMUM = 3, // skip grayscale passes — AA pixels stay solid black from BW pass
|
||||||
|
TEXT_DARKNESS_COUNT
|
||||||
|
};
|
||||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||||
enum IMAGE_DITHERING {
|
enum IMAGE_DITHERING {
|
||||||
IMAGE_DITHER_BAYER = 0,
|
IMAGE_DITHER_BAYER = 0,
|
||||||
@@ -191,6 +200,9 @@ class CrossPointSettings {
|
|||||||
// Text rendering settings
|
// Text rendering settings
|
||||||
uint8_t extraParagraphSpacing = 1;
|
uint8_t extraParagraphSpacing = 1;
|
||||||
uint8_t textAntiAliasing = 1;
|
uint8_t textAntiAliasing = 1;
|
||||||
|
// Text darkness (0 = normal, 1 = dark, 2 = extra dark). Default 1 preserves
|
||||||
|
// historical AA rendering (both grayscale shades drawn in the MSB pass).
|
||||||
|
uint8_t textDarkness = DARKNESS_DARK;
|
||||||
// Short power button click behaviour
|
// Short power button click behaviour
|
||||||
uint8_t shortPwrBtn = IGNORE;
|
uint8_t shortPwrBtn = IGNORE;
|
||||||
// EPUB reading orientation settings
|
// EPUB reading orientation settings
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
|||||||
StrId::STR_CAT_READER),
|
StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
||||||
StrId::STR_CAT_READER),
|
StrId::STR_CAT_READER),
|
||||||
|
SettingInfo::Enum(StrId::STR_TEXT_DARKNESS, &CrossPointSettings::textDarkness,
|
||||||
|
{StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK},
|
||||||
|
"textDarkness", StrId::STR_CAT_READER),
|
||||||
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
|
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
|
||||||
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
|
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
|
||||||
"imageRendering", StrId::STR_CAT_READER),
|
"imageRendering", StrId::STR_CAT_READER),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct MenuResult {
|
|||||||
uint8_t pageTurnOption = 0;
|
uint8_t pageTurnOption = 0;
|
||||||
int8_t embeddedStyleOverride = -1;
|
int8_t embeddedStyleOverride = -1;
|
||||||
int8_t imageRenderingOverride = -1;
|
int8_t imageRenderingOverride = -1;
|
||||||
|
uint8_t textDarkness = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ChapterResult {
|
struct ChapterResult {
|
||||||
|
|||||||
@@ -173,20 +173,21 @@ void EpubReaderActivity::loop() {
|
|||||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||||
}
|
}
|
||||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||||
startActivityForResult(
|
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||||
std::make_unique<EpubReaderMenuActivity>(
|
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
|
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
|
||||||
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride),
|
bookImageRenderingOverride, SETTINGS.textDarkness),
|
||||||
[this](const ActivityResult& result) {
|
[this](const ActivityResult& result) {
|
||||||
// Always apply orientation change even if the menu was cancelled
|
// Always apply orientation/darkness change even if the menu was cancelled
|
||||||
const auto& menu = std::get<MenuResult>(result.data);
|
const auto& menu = std::get<MenuResult>(result.data);
|
||||||
applyOrientation(menu.orientation);
|
applyOrientation(menu.orientation);
|
||||||
toggleAutoPageTurn(menu.pageTurnOption);
|
applyTextDarkness(menu.textDarkness);
|
||||||
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride);
|
toggleAutoPageTurn(menu.pageTurnOption);
|
||||||
if (!result.isCancelled) {
|
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride);
|
||||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
if (!result.isCancelled) {
|
||||||
}
|
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes to home screen
|
// Long press BACK (1s+) goes to home screen
|
||||||
@@ -587,6 +588,17 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpubReaderActivity::applyTextDarkness(const uint8_t textDarkness) {
|
||||||
|
if (SETTINGS.textDarkness == textDarkness) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SETTINGS.textDarkness = textDarkness;
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
renderer.setTextDarkness(textDarkness);
|
||||||
|
// Force a re-render so the new darkness is visible immediately.
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) {
|
void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) {
|
||||||
if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) {
|
if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) {
|
||||||
automaticPageTurnActive = false;
|
automaticPageTurnActive = false;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class EpubReaderActivity final : public Activity {
|
|||||||
void launchKOReaderSync(SyncLaunchMode mode = SyncLaunchMode::COMPARE);
|
void launchKOReaderSync(SyncLaunchMode mode = SyncLaunchMode::COMPARE);
|
||||||
void handleSyncResult(const ActivityResult& result);
|
void handleSyncResult(const ActivityResult& result);
|
||||||
void applyOrientation(uint8_t orientation);
|
void applyOrientation(uint8_t orientation);
|
||||||
|
void applyTextDarkness(uint8_t textDarkness);
|
||||||
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
||||||
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
|
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
|
||||||
bool getEffectiveEmbeddedStyle() const;
|
bool getEffectiveEmbeddedStyle() const;
|
||||||
|
|||||||
@@ -12,26 +12,29 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
|||||||
const std::string& title, const int currentPage, const int totalPages,
|
const std::string& title, const int currentPage, const int totalPages,
|
||||||
const int bookProgressPercent, const uint8_t currentOrientation,
|
const int bookProgressPercent, const uint8_t currentOrientation,
|
||||||
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
|
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
|
||||||
const int8_t initialImageRenderingOverride)
|
const int8_t initialImageRenderingOverride,
|
||||||
|
const uint8_t initialTextDarkness)
|
||||||
: Activity("EpubReaderMenu", renderer, mappedInput),
|
: Activity("EpubReaderMenu", renderer, mappedInput),
|
||||||
menuItems(buildMenuItems(hasFootnotes)),
|
menuItems(buildMenuItems(hasFootnotes)),
|
||||||
title(title),
|
title(title),
|
||||||
pendingOrientation(currentOrientation),
|
pendingOrientation(currentOrientation),
|
||||||
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
|
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
|
||||||
pendingImageRenderingOverride(initialImageRenderingOverride),
|
pendingImageRenderingOverride(initialImageRenderingOverride),
|
||||||
|
pendingTextDarkness(initialTextDarkness),
|
||||||
currentPage(currentPage),
|
currentPage(currentPage),
|
||||||
totalPages(totalPages),
|
totalPages(totalPages),
|
||||||
bookProgressPercent(bookProgressPercent) {}
|
bookProgressPercent(bookProgressPercent) {}
|
||||||
|
|
||||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
||||||
std::vector<MenuItem> items;
|
std::vector<MenuItem> items;
|
||||||
items.reserve(12);
|
items.reserve(13);
|
||||||
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
||||||
if (hasFootnotes) {
|
if (hasFootnotes) {
|
||||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
||||||
}
|
}
|
||||||
items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE});
|
items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE});
|
||||||
items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES});
|
items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES});
|
||||||
|
items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS});
|
||||||
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
||||||
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
|
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
|
||||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||||
@@ -106,15 +109,25 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedAction == MenuAction::TEXT_DARKNESS) {
|
||||||
|
pendingTextDarkness = (pendingTextDarkness + 1) % textDarknessLabels.size();
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption,
|
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption,
|
||||||
pendingEmbeddedStyleOverride, pendingImageRenderingOverride});
|
pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness});
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
ActivityResult result;
|
ActivityResult result;
|
||||||
result.isCancelled = true;
|
result.isCancelled = true;
|
||||||
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride,
|
result.data = MenuResult{-1,
|
||||||
pendingImageRenderingOverride};
|
pendingOrientation,
|
||||||
|
selectedPageTurnOption,
|
||||||
|
pendingEmbeddedStyleOverride,
|
||||||
|
pendingImageRenderingOverride,
|
||||||
|
pendingTextDarkness};
|
||||||
setResult(std::move(result));
|
setResult(std::move(result));
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
@@ -191,6 +204,13 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
|
|||||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
||||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected);
|
renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (menuItems[i].action == MenuAction::TEXT_DARKNESS) {
|
||||||
|
const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0;
|
||||||
|
const char* value = I18N.get(textDarknessLabels[idx]);
|
||||||
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer / Hints
|
// Footer / Hints
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class EpubReaderMenuActivity final : public Activity {
|
|||||||
FOOTNOTES,
|
FOOTNOTES,
|
||||||
EMBEDDED_STYLE,
|
EMBEDDED_STYLE,
|
||||||
IMAGE_RENDERING,
|
IMAGE_RENDERING,
|
||||||
|
TEXT_DARKNESS,
|
||||||
GO_TO_PERCENT,
|
GO_TO_PERCENT,
|
||||||
AUTO_PAGE_TURN,
|
AUTO_PAGE_TURN,
|
||||||
ROTATE_SCREEN,
|
ROTATE_SCREEN,
|
||||||
@@ -30,8 +31,8 @@ class EpubReaderMenuActivity final : public Activity {
|
|||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||||
const uint8_t currentOrientation, const bool hasFootnotes,
|
const uint8_t currentOrientation, const bool hasFootnotes,
|
||||||
const int8_t initialEmbeddedStyleOverride,
|
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
|
||||||
const int8_t initialImageRenderingOverride);
|
const uint8_t initialTextDarkness);
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
@@ -57,8 +58,11 @@ class EpubReaderMenuActivity final : public Activity {
|
|||||||
uint8_t selectedPageTurnOption = 0;
|
uint8_t selectedPageTurnOption = 0;
|
||||||
int8_t pendingEmbeddedStyleOverride = -1;
|
int8_t pendingEmbeddedStyleOverride = -1;
|
||||||
int8_t pendingImageRenderingOverride = -1;
|
int8_t pendingImageRenderingOverride = -1;
|
||||||
|
uint8_t pendingTextDarkness = 1;
|
||||||
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
|
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
|
||||||
StrId::STR_LANDSCAPE_CCW};
|
StrId::STR_LANDSCAPE_CCW};
|
||||||
|
const std::vector<StrId> textDarknessLabels = {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK,
|
||||||
|
StrId::STR_MAX_DARK};
|
||||||
const std::vector<StrId> imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
|
const std::vector<StrId> imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
|
||||||
StrId::STR_IMAGES_SUPPRESS};
|
StrId::STR_IMAGES_SUPPRESS};
|
||||||
const std::vector<const char*> pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"};
|
const std::vector<const char*> pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"};
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ void loop() {
|
|||||||
HalClock::updatePeriodic();
|
HalClock::updatePeriodic();
|
||||||
|
|
||||||
renderer.setFadingFix(SETTINGS.fadingFix);
|
renderer.setFadingFix(SETTINGS.fadingFix);
|
||||||
|
renderer.setTextDarkness(SETTINGS.textDarkness);
|
||||||
|
|
||||||
if (Serial && millis() - lastMemPrint >= 10000) {
|
if (Serial && millis() - lastMemPrint >= 10000) {
|
||||||
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
|
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
|
||||||
|
|||||||
Reference in New Issue
Block a user