takkaO / OpenFontRender

TTF font render support library for microcomputer.
Other
105 stars 16 forks source link

Getting flickering with large front redraws #36

Closed scottchiefbaker closed 7 months ago

scottchiefbaker commented 7 months ago

I'm attempting to build a count-up timer with a large font (160px+). When I redraw the screen once per second I get a visible flicker when I draw over the previous value. I have tried using BgFillMethod::Block and drawing a large rectangle over the previous text, and both have the same visual flicker. Is this a limitation of using large fonts? Is it even possible to render a large font without flicker on an ESP32?

#include "NotoSans_Bold.h"
#include "OpenFontRender.h"

#define TTF_FONT NotoSans_Bold // The font is referenced with the array name
OpenFontRender ofr;

#include <SPI.h>
#include <TFT_eSPI.h>

TFT_eSPI tft = TFT_eSPI();

void setup(void) {
    tft.begin();
    tft.setRotation(1);

    ofr.setDrawer(tft); // Link drawing object to tft instance (so font will be rendered on TFT)
    ofr.loadFont(TTF_FONT, sizeof(TTF_FONT));
}

void loop() {
    tft.fillScreen(TFT_BLACK);

    for (int i = 0; i <= 1000; i++) {
        ofr.setCursor(10, 10);
        ofr.setFontColor(TFT_WHITE, TFT_BLACK);
        ofr.setFontSize(200);

        //ofr.setBackgroundFillMethod(BgFillMethod::Block);
        tft.fillRect(10, 10, 120, 120, TFT_BLACK);

        ofr.printf("%02d", i);

        delay(1000);
    }
}
takkaO commented 7 months ago

Thank you for using 😊

Looking at the code you provided, the problem is probably tft.fillRect in the loop. In your code, every time you update a character, you repaint it in black and then rewrite it. This is the cause of the flickering.

To fix it, try removing tft.fillRect and set setBackgroundFillMethod(BgFillMethod::Block.

void setup(void) {
    tft.begin();
    tft.setRotation(1);

    ofr.setDrawer(tft); // Link drawing object to tft instance (so font will be rendered on TFT)
    ofr.loadFont(TTF_FONT, sizeof(TTF_FONT));

    ofr.setBackgroundFillMethod(BgFillMethod::Block);    // Add
}

void loop() {
    tft.fillScreen(TFT_BLACK);

    for (int i = 0; i <= 1000; i++) {
        ofr.setCursor(10, 10);
        ofr.setFontColor(TFT_WHITE, TFT_BLACK);
        ofr.setFontSize(200);

        //ofr.setBackgroundFillMethod(BgFillMethod::Block);
        //tft.fillRect(10, 10, 120, 120, TFT_BLACK);    // Remove

        ofr.printf("%02d", i);

        delay(1000);
    }
}

If this does not stop the flickering, you may want to consider using the Sprite function.

scottchiefbaker commented 7 months ago

Using BgFillMethod::Block doesn't seem to solve my flickering issue. I get ghosting from the previous character and I still see the flickering on each character change.

https://github.com/takkaO/OpenFontRender/assets/3429760/e9880366-2aff-453a-bfa8-5f58f201c567

Am I missing something obvious?

chconnor commented 7 months ago

@scottchiefbaker - You're using TFT_eSPI? If you have enough memory, make a sprite and use that. So every time you want to change the numeral, you will:

You'll need to make an appropriately-sized sprite, so you'll have to figure out the rectangular bounds of a character. So, something like: static TFT_eSprite mysprite = TFT_eSprite(&tft); ... in setup(): void *out = mysprite.createSprite(SPRITE_WIDTH, SPRITE_HEIGHT); if (out == NULL) Serial.println("Could not allocate memory for sprite!");

I'm just a beginner, but this is my understanding of how to do this with minimal flicker. (Sprites don't draw instantly, but I don't think it gets any faster than that -- the limit will be the time it takes to push pixels to the screen.)

scottchiefbaker commented 7 months ago

@chconnor using a sprite actually worked great! Now my updates are smooth.

I did run in to hiccup with setAlignment() which doesn't appear to work with a sprite? I want my text to align to the lower right of my sprite. Anytime I put ofr.setAlignment(Align::BottomRight) (or any alignment) no text renders at all. As soon as I comment the alignment out it works again.

Is there a better way to align text to the lower right?

chconnor commented 7 months ago

When you set Align::BottomRight, are you are adjusting the coordinates of where you are drawing? E.g. if you are doing setCursor(0,0) with Align::TopLeft, when you are doing Align::BottomRight you want to change that to setCursor(x,y) where x and y are the width and height of the sprite (i.e. the coordinates of the bottom right of the sprite). If you aren't doing that, it makes sense that nothing would appear, because the cursor being at 0,0 would make everything show up above and to the left of the bounds of the sprite.

(If you are using drawChar or drawString, analogous logic applies.)

That said, at least with the font that I use, I've had some minor alignment issues with TopRight and BottomRight (see my issue here) so you might be better off with TopLeft and BottomLeft if you can make those work, if you need consistent alignment. (Or it might just be me.)

scottchiefbaker commented 7 months ago

@chconnor thank you for your help with the Sprite method. Once I learned that, I was able to get things work correctly. I'll submit a PR for a SmoothFont example here in a little bit. Just so that it's archived here is the sample code I wrote to output millis() on the screen.

#include "OpenFontRender.h"
OpenFontRender ofr;

#include "NotoSans_Bold.h"
#define TTF_FONT NotoSans_Bold

#include <SPI.h>
#include <TFT_eSPI.h>

TFT_eSPI tft       = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);

void setup(void) {
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);

  ofr.setDrawer(sprite);
  ofr.loadFont(TTF_FONT, sizeof(TTF_FONT));
}

void loop() { 
  void *out = sprite.createSprite(140, 50);
  if (out == NULL) Serial.println("Could not allocate memory for sprite!");
  sprite.fillSprite(TFT_RED);

  ofr.setFontSize(64);
  ofr.setCursor(sprite.width() / 2, sprite.height() / 2); // Middle of box
  ofr.setAlignment(Align::MiddleCenter);
  ofr.setFontColor(TFT_WHITE, TFT_BLACK);
  ofr.printf("%d", millis());

  int hcenter = (tft.width()  / 2) - (sprite.width()  / 2);
  int vcenter = (tft.height() / 2) - (sprite.height() / 2);

  sprite.pushSprite(hcenter,vcenter);
}