Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1f57fb9
PoC working but just a hack
DedeHai Feb 14, 2026
0ae6fbb
full refactoring, partially working
DedeHai Feb 14, 2026
e1ae3e2
refactoring, fixed some bugs
DedeHai Feb 14, 2026
a81d5cf
add new tiny font, remove debug output,
DedeHai Feb 14, 2026
cada48b
fixes, optimizations still not fully working
DedeHai Feb 15, 2026
a09e296
add support for flash fonts back, unified functions. custom glyphs cu…
DedeHai Feb 15, 2026
c6e10f9
minor fix
DedeHai Feb 15, 2026
1339593
bugfix, now file fonts work again with extended chars
DedeHai Feb 15, 2026
0f978bc
lost line
DedeHai Feb 15, 2026
f61cbd7
another
DedeHai Feb 15, 2026
f729dc0
update to latest header format using 12 bytes
DedeHai Feb 15, 2026
e50dc58
fixed up the fonts
DedeHai Feb 15, 2026
5d3c554
fix off by one, new font uses full 0-127 ASCII
DedeHai Feb 15, 2026
619d700
bugfix, update some comments
DedeHai Feb 15, 2026
76a3ea4
fix reasonable rabbit suggestions
DedeHai Feb 16, 2026
3962c6b
code cleanup, WIP
DedeHai Feb 24, 2026
da492b1
add font file helper, also cache flash fonts (reduces complexity)
DedeHai Feb 24, 2026
2fec051
update built-in fonts with similar but nicer ones
DedeHai Feb 25, 2026
f75a570
some code claenaup
DedeHai Feb 25, 2026
c66d697
started code cleanup
DedeHai Feb 26, 2026
3804f3e
more code cleanup, remove obsolete variables and functions
DedeHai Feb 26, 2026
f8d0a5c
prevent crash if font file is invalid
DedeHai Feb 26, 2026
6092ff7
reorder functions, minor cleanup
DedeHai Feb 26, 2026
d355ab4
bugfixes in FX calculations (size/position) rename & fix font
DedeHai Feb 26, 2026
4970c41
minor fixes
DedeHai Feb 27, 2026
087c06b
add support for any wbf file name, dropping the fixed name scheme (at…
DedeHai Feb 27, 2026
a62be1b
remove drawCharacter() declarations from segment, fix overflow with u…
DedeHai Feb 27, 2026
7606ac6
add padding to glyph entry
DedeHai Feb 27, 2026
8698089
fix accidental removal of bitmap
DedeHai Feb 27, 2026
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
125 changes: 84 additions & 41 deletions wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6304,30 +6304,11 @@ static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;
////////////////////////////
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 or no segment name set
char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'};
size_t result_pos = 0;
char sec[5];
Expand All @@ -6341,10 +6322,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 Down Expand Up @@ -6387,7 +6371,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 +6383,45 @@ 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
if (!fontManager.loadFont(fontNum, text, useCustomFont)) return; // note: FontManageraccess can lead to crashes if font loading fails due to low heap

// Get font dimensions
uint8_t fontHeight = fontManager.getFontHeight();
uint8_t fontWidth = fontManager.getFontWidth(); // for fonts with variable width, this is the max letter width
uint8_t letterSpacing = isRotated ? 1 : fontManager.getFontSpacing(); // when rotated use spacing of 1, otherwise use font defined spacing

// 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 += fontHeight + letterSpacing; // use height when rotated, spacing of 1
} else {
totalTextWidth += fontManager.getGlyphWidth(unicode) + 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 +6431,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; // text fits, position it at the 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 +6461,33 @@ 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;
int unrotatedWidth = fontManager.getGlyphWidth(unicode);
int glyphWidth = isRotated ? fontHeight : unrotatedWidth; // use font height for width if 90° rotated
int glyphHeight = isRotated ? unrotatedWidth : fontHeight; // use (variable) glyph-width for height if 90° rotated
int drawX = int(cols) - int(SEGENV.aux0) + currentXOffset; // aux0 is (scrolling) offset, no offset position is right side boarder (cols)
if (drawX >= cols) break; // skip if character is off-screen on the right
int advance = glyphWidth + letterSpacing;

if (drawX + advance < 0) {
currentXOffset += advance;
continue; // Skip if off-screen on the left
}

int16_t drawY = yoffset + (rows - glyphHeight) / 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
119 changes: 115 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,10 @@ 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 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 +810,6 @@ 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 wu_pixel(uint32_t x, uint32_t y, CRGB c) {}
#endif
friend class WS2812FX;
Expand Down Expand Up @@ -1066,4 +1064,117 @@ class WS2812FX {
extern const char JSON_mode_names[];
extern const char JSON_palette_names[];

#define LAST_ASCII_CHAR 127
#define FONT_HEADER_SIZE 12
/**
* Font format:
*
* 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 omitted for fixed width fonts i.e. glyphs start after header
* - 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
uint8_t reserved; // Padding to keep FontHeader 4-byte aligned
};

// Segment metadata (stored BEFORE the font data in segment data)
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, highest bit set = file font)
uint8_t lastFontNum; // font number requested in last call
uint8_t glyphCount; // Number of glyphs cached
};

// Memory layout of cached font in segment data:
// [SegmentFontMetadata] - 4 bytes
// [GlyphEntry array]
// [12-byte font header] - copy of the relevant font header data
// [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 = 64; // font names

// font header, identical to wbf header, size must be FONT_HEADER_SIZE
struct FontHeader {
uint8_t magic; // should be 'W' (0x57)
uint8_t height; // TODO: should we use the padding bytes and store a full copy of the header? might make copying the header easier?
uint8_t width;
uint8_t spacing;
uint8_t flags;
uint8_t first;
uint8_t last;
uint8_t reserved; // should be 0x00
uint32_t firstUnicode;
};
static_assert(sizeof(FontHeader) == FONT_HEADER_SIZE, "FontHeader size must be exactly FONT_HEADER_SIZE bytes");

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

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

// Get dimensions (use cached header)
inline uint8_t getFontHeight() { return reinterpret_cast<FontHeader*>(_fontBase)->height; }
inline uint8_t getFontWidth() { return reinterpret_cast<FontHeader*>(_fontBase)->width; }
inline uint8_t getFontSpacing() { return reinterpret_cast<FontHeader*>(_fontBase)->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;
uint8_t _fontNum; // Font number (0-4)
bool _useFileFont; // true = file, false = flash
bool _cacheNumbers;
uint8_t* _fontBase; // pointer to start of font data (header + bitmaps) in segment data

// get metadata pointer
SegmentFontMetadata* getMetadata() {
return (SegmentFontMetadata*)_segment->data;
}

void updateFontBase() {
SegmentFontMetadata* meta = getMetadata();
// font data (header + glyph bitmaps) starts after metadata + registry
_fontBase = _segment->data + sizeof(SegmentFontMetadata) + (meta->glyphCount * sizeof(GlyphEntry));
}

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

// Glyph index calculation (pure function, inline for speed)
int32_t getGlyphIndex(uint32_t unicode, FontHeader* hdr);

// File font management
void getFontFileName(uint8_t fontNum, char* buffer, bool scanAll = false);
void scanAvailableFonts();
void rebuildCache(const char* text);
uint8_t collectNeededCodes(const char* text, FontHeader* hdr, uint8_t* outCodes);
};

#endif
Loading