Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
9474de3
add comment
DedeHai Mar 7, 2026
2627cc1
refactor: move fontmanager into its own files
DedeHai Mar 17, 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
126 changes: 85 additions & 41 deletions wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "wled.h"
#include "FX.h"
#include "fontmanager.h"
#include "fcn_declare.h"

#define FX_FALLBACK_STATIC { mode_static(); return; }
Expand Down Expand Up @@ -6304,30 +6305,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 +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 Down Expand Up @@ -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,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 +6432,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 +6462,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
6 changes: 2 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
49 changes: 3 additions & 46 deletions wled00/FX_2Dfcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -559,51 +559,6 @@ void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint3
}
}

#include "src/font/console_font_4x6.h"
#include "src/font/console_font_5x8.h"
#include "src/font/console_font_5x12.h"
#include "src/font/console_font_6x8.h"
#include "src/font/console_font_7x9.h"

// draws a raster font character on canvas
// only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM
void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) const {
if (!isActive()) return; // not active
if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported
chr -= 32; // align with font table entries
const int font = w*h;

// if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2
CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient

for (int i = 0; i<h; i++) { // character height
uint8_t bits = 0;
switch (font) {
case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); break; // 4x6 font
case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); break; // 5x8 font
case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); break; // 6x8 font
case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); break; // 7x9 font
case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font
default: return;
}
CRGBW c = ColorFromPalette(grad, (i+1)*255/h, 255, LINEARBLEND_NOWRAP); // NOBLEND is faster
for (int j = 0; j<w; j++) { // character width
int x0, y0;
switch (rotate) {
case -1: x0 = x + (h-1) - i; y0 = y + (w-1) - j; break; // -90 deg
case -2:
case 2: x0 = x + j; y0 = y + (h-1) - i; break; // 180 deg
case 1: x0 = x + i; y0 = y + j; break; // +90 deg
default: x0 = x + (w-1) - j; y0 = y + i; break; // no rotation
}
if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen
if (((bits>>(j+(8-w))) & 0x01)) { // bit set
setPixelColorXYRaw(x0, y0, c.color32);
}
}
}
}

#define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8))
void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu
if (!isActive()) return; // not active
Expand All @@ -626,4 +581,6 @@ void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu
}
#undef WU_WEIGHT

#endif // WLED_DISABLE_2D


#endif // WLED_DISABLE_2D
2 changes: 2 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ size_t printSetFormIndex(Print& settingsScript, const char* key, int index);
size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val);
void prepareHostname(char* hostname);
[[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen);
uint32_t utf8_decode(const char *s, uint8_t *len);
size_t utf8_strlen(const char *s);
bool requestJSONBufferLock(uint8_t moduleID=JSON_LOCK_UNKNOWN);
void releaseJSONBufferLock();
uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen);
Expand Down
Loading
Loading