Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 86 additions & 42 deletions wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6302,32 +6302,14 @@ static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;
////////////////////////////
// 2D Scrolling text //
////////////////////////////

void mode_2Dscrollingtext(void) {
if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up

FontManager fontManager(&SEGMENT);
const int cols = SEG_W;
const int rows = SEG_H;

unsigned letterWidth, rotLW;
unsigned letterHeight, rotLH;
switch (map(SEGMENT.custom2, 0, 255, 1, 5)) {
default:
case 1: letterWidth = 4; letterHeight = 6; break;
case 2: letterWidth = 5; letterHeight = 8; break;
case 3: letterWidth = 6; letterHeight = 8; break;
case 4: letterWidth = 7; letterHeight = 9; break;
case 5: letterWidth = 5; letterHeight = 12; break;
}
// letters are rotated
const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2);
if (rotate == 1 || rotate == -1) {
rotLH = letterWidth;
rotLW = letterHeight;
} else {
rotLW = letterWidth;
rotLH = letterHeight;
}

// generate time/date if there are any # tokens
char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'};
size_t result_pos = 0;
char sec[5];
Expand All @@ -6341,10 +6323,13 @@ void mode_2Dscrollingtext(void) {
sprintf_P(sec, PSTR(":%02d"), second(localTime));
}

// prepare text string from segment name
size_t len = 0;
if (SEGMENT.name) len = strlen(SEGMENT.name); // note: SEGMENT.name is limited to WLED_MAX_SEGNAME_LEN
if (len == 0) { // fallback if empty segment name: display date and time
if (len == 0) {
// fallback if empty segment name: display date and time "#MON #DD #YYYY #TIME"
sprintf_P(text, PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec);
fontManager.cacheNumbers(true); // cache all numbers when using clock to avoid frequent re-caching
} else {
size_t i = 0;
while (i < len) {
Expand All @@ -6361,7 +6346,7 @@ void mode_2Dscrollingtext(void) {
token[j] = '\0';
int advance = 5; // number of chars to advance in 'text' after processing the token

// Process token
// process token
char temp[32];
if (!strncmp_P(token,PSTR("#DATE"),5)) sprintf_P(temp, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime));
else if (!strncmp_P(token,PSTR("#DDMM"),5)) sprintf_P(temp, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime));
Expand All @@ -6387,7 +6372,7 @@ void mode_2Dscrollingtext(void) {
strcpy(text + result_pos, temp);
result_pos += temp_len;
}

fontManager.cacheNumbers(true); // cache all numbers when using clocks to avoid frequent re-caching
i += advance;
}
else {
Expand All @@ -6399,11 +6384,46 @@ void mode_2Dscrollingtext(void) {
}
}

const int numberOfLetters = strlen(text);
int width = (numberOfLetters * rotLW);
int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-rotLH)/2;
if (width <= cols) {
// scroll vertically (e.g. ^^ Way out ^^) if it fits
// Font selection
bool useCustomFont = SEGMENT.check2;
uint8_t fontNum = map(SEGMENT.custom2, 0, 255, 0, 4);

// letters orientation: -2/+2 = upside down, -1 = 90° clockwise, 0 = normal, 1 = 90° counterclockwise
const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2);
const bool isRotated = (rotate == 1 || rotate == -1); // +/- 90° rotated, swap width and height for calculations

// Load the font
fontManager.loadFont(fontNum, text, useCustomFont);

// Get font dimensions
uint8_t glyphHeight = fontManager.getFontHeight();
uint8_t fontWidth = fontManager.getFontWidth(); // for fonts with variable width, this is the max letter width
uint8_t letterSpacing = fontManager.getFontSpacing();

// Calculate total text width
int totalTextWidth = 0;
int idx = 0;
const int numberOfChars = utf8_strlen(text);

for (int c = 0; c < numberOfChars; c++) {
uint8_t charLen;
uint32_t unicode = utf8_decode(&text[idx], &charLen);
idx += charLen;

if (isRotated) {
totalTextWidth += glyphHeight + 1; // use height when rotated, spacing of 1
} else {
totalTextWidth += fontManager.getGlyphWidth(unicode) + letterSpacing;
}
if (c < numberOfChars - 1) totalTextWidth += letterSpacing;
}
totalTextWidth -= letterSpacing; // remove spacing after last character

// y-offset calculation
int yoffset = map(SEGMENT.intensity, 0, 255, -rows / 2, rows / 2);

if (totalTextWidth <= cols) {
// if text fits matrix width, scroll vertically
int speed = map(SEGMENT.speed, 0, 255, 5000, 1000);
int frac = strip.now % speed + 1;
if (SEGMENT.intensity == 255) {
Expand All @@ -6413,21 +6433,26 @@ void mode_2Dscrollingtext(void) {
}
}

// scroll step (AUX0 is current scrolling offset)
if (SEGENV.step < strip.now) {
// calculate start offset
if (width > cols) {
if (SEGMENT.check3) {
if (SEGENV.aux0 == 0) SEGENV.aux0 = width + cols - 1;
else --SEGENV.aux0;
} else ++SEGENV.aux0 %= width + cols;
} else SEGENV.aux0 = (cols + width)/2;
if (totalTextWidth > cols) {
if (SEGMENT.check3) { // reverse direction
if (SEGENV.aux0 == 0) SEGENV.aux0 = totalTextWidth + cols - 1;
else --SEGENV.aux0;
} else {
++SEGENV.aux0 %= totalTextWidth + cols;
}
} else {
SEGENV.aux0 = (cols - totalTextWidth) / 2; // Center
}
++SEGENV.aux1 &= 0xFF; // color shift
SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms
SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50);
}

SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail
uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0);
uint32_t col2 = BLACK;

// if gradient is selected and palette is default (0) drawCharacter() uses gradient from SEGCOLOR(0) to SEGCOLOR(2)
// otherwise col2 == BLACK means use currently selected palette for gradient
// if gradient is not selected set both colors the same
Expand All @@ -6438,13 +6463,32 @@ void mode_2Dscrollingtext(void) {
}
} else col2 = col1; // force characters to use single color (from palette)

for (int i = 0; i < numberOfLetters; i++) {
int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i;
if (xoffset + rotLW < 0) continue; // don't draw characters off-screen
SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, rotate);
// Draw characters
idx = 0;
int currentXOffset = 0; // offset of current glyph from text start

for (int c = 0; c < numberOfChars; c++) {
uint8_t charLen;
uint32_t unicode = utf8_decode(&text[idx], &charLen);
idx += charLen;
uint8_t glyphWidth = fontManager.getGlyphWidth(unicode);
int drawX = int(cols) - int(SEGENV.aux0) + currentXOffset; // AUX0 is scrolling offset
int advance = isRotated ? glyphHeight + 1 : glyphWidth + letterSpacing; // when rotated use spacing of 1

// Skip if off-screen
if (drawX + advance < 0) {
currentXOffset += advance;
continue;
}
if (drawX >= cols) break;
unsigned rotHeight = isRotated ? glyphWidth : glyphHeight; // use (variable) glyph-width for height if 90° rotated
int16_t drawY = yoffset + (rows - rotHeight) / 2; // center glyph vertically

fontManager.drawCharacter(unicode, drawX, drawY, col1, col2, rotate);
currentXOffset += advance;
}
}
static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0";
static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,Custom Font,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0";


////////////////////////////
Expand Down
144 changes: 140 additions & 4 deletions wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,12 @@ typedef enum mapping1D2D {
} mapping1D2D_t;

class WS2812FX;
class FontManager;

// segment, 76 bytes
class Segment {
public:
friend class FontManager; // Allow FontManager to access protected members
uint32_t colors[NUM_COLORS];
uint16_t start; // start index / start X coordinate 2D (left)
uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0
Expand Down Expand Up @@ -770,12 +772,11 @@ class Segment {
void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;
void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;
void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const;
void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const;
void wu_pixel(uint32_t x, uint32_t y, CRGB c) const;
inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); }
inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); }
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline
inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(unicode, x, y, fontData, fontFile, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline
inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); }
#else
inline bool is2D() const { return false; }
Expand Down Expand Up @@ -810,8 +811,8 @@ class Segment {
inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {}
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {}
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {}
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {}
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {}
inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) {}
inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) {}
inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {}
#endif
friend class WS2812FX;
Expand Down Expand Up @@ -1066,4 +1067,139 @@ class WS2812FX {
extern const char JSON_mode_names[];
extern const char JSON_palette_names[];

#define LAST_ASCII_CHAR 127
#define FONT_HEADER_SIZE 12
/**
* Unified Font Format (Flash and RAM use IDENTICAL layout)
*
* Header Layout (12 Bytes):
* [0] Magic 'W' (0x57)
* [1] Glyph height
* [2] Fixed/max glyph width
* [3] Spacing between chars
* [4] Flags: (0x01 = variable width)
* [5] First Char
* [6] Last Char
* [7] reserved: 0x00
* [8-11] Unicode Offset (32-bit little-endian)
*
* Followed by:
* - Width table (if variable width): [first..last] byte array
* - Bitmap data: bit-packed glyphs - top left to bottom right, row by row, MSB first, see src/font files for example
*/


// Glyph entry in RAM cache
struct GlyphEntry {
uint8_t code; // Glyph index (0-255)
uint8_t width; // Width in pixels
uint8_t height; // Height in pixels
};

// Segment metadata (stored BEFORE the font data in RAM)
struct SegmentFontMetadata {
uint8_t availableFonts; // Bitflags for available fonts: set to 1 << fontNum if font is available in FS (0-4)
uint8_t cachedFontNum; // Currently cached font (0-4, 0xFF = none)
uint8_t fontsScanned; // 1 if filesystem scanned
uint8_t glyphCount; // Number of glyphs cached
};

// Memory layout for cached fonts:
// [SegmentFontMetadata] - 4 bytes
// [GlyphEntry array] - 4 bytes each
// [12-byte font header] - for compatibility and to store font info
// [Bitmap data] - sequential, matches registry order

static constexpr uint8_t MAX_CACHED_GLYPHS = 64; // max segment string length is 64 chars so this is absolute worst case
static constexpr uint8_t MAX_FONTS = 5; // scrolli text supports font numbers 0-4
static constexpr size_t FONT_NAME_BUFFER_SIZE = 16; // font names is /fontX.wbf

// Font header structure
struct FontHeader {
uint8_t height;
uint8_t width;
uint8_t spacing;
uint8_t flags;
uint8_t first;
uint8_t last;
uint32_t firstUnicode;
};

class FontManager {
public:
FontManager(Segment* seg) :
_segment(seg),
_flashFont(nullptr),
_fontNum(0),
_useFlashFont(false),
_cacheNumbers(false),
_headerValid(false),
_fontBase(nullptr) {}

bool loadFont(uint8_t fontNum, const char* text, bool useFile);
void cacheNumbers(bool cache) { _cacheNumbers = cache; }
void prepare(const char* text);

inline void beginFrame() {
if (!_headerValid) {
updateFontBase();
if (_fontBase) {
parseHeader();
}
}
}

// Get dimensions (use cached header)
inline uint8_t getFontHeight() { return _cachedHeader.height; }
inline uint8_t getFontWidth() { return _cachedHeader.width; }
inline uint8_t getFontSpacing() { return _cachedHeader.spacing; }
uint8_t getGlyphWidth(uint32_t unicode);

// Rendering
void drawCharacter(uint32_t unicode, int16_t x, int16_t y, uint32_t color, uint32_t col2, int8_t rotate);

private:
Segment* _segment;
const uint8_t* _flashFont;
uint8_t _fontNum; // Font number (0-4)
bool _useFlashFont; // true = flash, false = file
bool _cacheNumbers;

// Cached data for performance (non-static, per-instance)
bool _headerValid;
FontHeader _cachedHeader;
const uint8_t* _fontBase;

// Invalidate cached header (call when font changes)
inline void invalidateHeader() {
_headerValid = false;
}

inline void updateFontBase() {
if (_segment->data) {
SegmentFontMetadata* meta = (SegmentFontMetadata*)_segment->data;
// Font header starts after metadata + registry
_fontBase = _segment->data + sizeof(SegmentFontMetadata) + (meta->glyphCount * sizeof(GlyphEntry));
} else {
_fontBase = nullptr;
}
}

// Metadata access (RAM only)
SegmentFontMetadata* getMetadata() {
return _segment->data ? (SegmentFontMetadata*)_segment->data : nullptr;
}

void parseHeader();
const uint8_t* getGlyphBitmap(uint32_t unicode, uint8_t& outWidth, uint8_t& outHeight);

// Glyph index calculation (pure function, inline for speed)
static inline int32_t getGlyphIndex(uint32_t unicode, uint8_t first, uint8_t last, uint32_t firstUnicode);

// File font management
void scanAvailableFonts();
void rebuildCache(const char* text);
uint8_t collectNeededCodes(const char* text, const FontHeader& hdr, uint8_t* outCodes);
};

#endif
Loading