Is there a way to anchor text to the middle of the screen?

Is there a way to anchor the text to the middle of the screen? Normally when a number increases in size, and eventually digits, it all gets pushed to the right. But how would I even all that out so the text stays put, and the “offset” gets distributed on both ends?

Also, not really related, but is there a way to erase (or how Sprites::drawErase() works) basic shapes which were created with functions such as arduboy.fillRect() or arduboy.fillCircle()?

Thanks!

Centering text dynamically is tough. There is no pre-built function for this you’ve got to count how long is your character array and then adjust the cursor. I did it in new blocks on the kid:

    strcpy_P(buffer, (char *)pgm_read_word(&(badWords[ endWord ])));
    int halfWordWidth = ((bufferLength(buffer) * 12) + 1) / 2;
    arduboy.setCursor(64 - halfWordWidth, 25);

In this case I’m using font size 2 so each character is 12 pixels wide.

1 Like

To centre text to the middle of the screen you’d have to:

  • Count the number of characters
  • Multiply that by the character width of the font, factoring in spaces
  • Calculate the proper X coordinate for the text by subtracting the previous value from the screen width, then dividing that by 2

If you were doing this with ordinary text you’d have to go through the string and count the number of characters, but the fact you’re doing this with an integer means you can take a short-cut because you can determine the number of decimal digits that will be required to represent the integer using just a few simple tests…

To count the number of digits in an integer you can do:

uint8_t countDigits(uint8_t value)
{
	if(value > 100)
		return 3;
	
	if(value > 10)
		return 2;
	
	return 1;
}

uint8_t countDigits(int16_t value)
{
	if(value > 10000)
		return 5;
		
	if(value > 1000)
		return 4;
		
	if(value > 100)
		return 3;
	
	if(value > 10)
		return 2;
	
	return 1;
}

Depending on what type you’re using.

(Note: a for loop is technically more scalable, but for integers this small the list of ifs is cheaper.)

The width of the default font is 5, and the inter-character spacing is 1, thus the second step can be achieved with:

uint8_t calculateStringWidth(uint8_t length)
{
	constexpr uint8_t characterWidth = 5;
	constexpr uint8_t characterSpace = 1;

	return ((length * characterWidth) + ((length - 1) * characterSpace));
}

And lastly you’d just need:

uint8_t calculateCentredX(uint8_t width)
{
	return ((Arduboy2::width() - width) / 2);
}

Chaining those all together gets you:

uint8_t calculateCentredXForInteger(uint8_t value)
{
	auto digits = countDigits(value);
	auto width = calculateStringWidth(digits);
	auto x = calculateCentredX(width);
	
	return x;
}

Which should tell you where to position the cursor to have an integer centred on the screen.

(Note: I haven’t actually checked the arithmetic here, I’m going primarily by memory.)


I’m not sure what you mean by this exactly.

If you drew a white rectangle then to get rid of that rectangle you could just draw a black one on top of it.

Are you hoping for a rectangle with transparency, or are you trying to draw just the outline of a rectangle or something?

1 Like

By the way, you don’t actually need that strcpy_P.

You can actually get rid of the need for the buffer by doing:

const char * string = reinterpret_cast<const char *>(pgm_read_ptr(&badWords[endWord]));
size_t halfWordWidth = (((strlen_P(string) * 12) + 1) / 2);
arduboy.setCursor(64 - static_cast<int>(halfWordWidth), 25);
arduboy.setTextSize(2);
arduboy.print(reinterpret_cast<const __FlashStringHelper *>(string));

(The main advantage here being that you no longer need to worry about potentially overflowing the buffer.)

Thanks everyone!

But if there was something behind, the black pixels would still show the shape of the object.

In my game, the player is created using arduboy.fillCircle(), and I want the player to be erased if collision occurs (and then the particles come in, but that’s for later). It’ll also make it look more natural since the object won’t visibly be inside the pipe (since I’m using arduboy.collide()).

I’m not sure what’s going on here. Why is the parameter value being used as countDigits()'s parameter value? And what are the parameter variables of the other functions? Also, would you not use the output of countDigits() to calculate width?

It’s better to use the Arduboy2 library functions to determine these values:
getCharacterWidth()
getCharacterSpacing()
getTextSize()

(There is also):
setTextSize()
getCharacterHeight()
getLineSpacing()

2 Likes

That’s a good idea. For me personally, I’m going to be putting this into separate files (which means no arduboy2 library, unless I have to include it again) so I’m going to have to use variables. Although arduboy.setTextSize() multiplies the text, so if it’s set to 2, then it’s just 5 * 2 (10). I would assume the sizes also get multiplied, so it’d be pretty easy to figure out all the sizes and such.

You should be including the library in all your code. There’s no downside to doing this.

2 Likes

Oh right reinterpret_cast<const char *> of course :rofl:

(I’ve literally never seen anything like that, but I suppose that’s why you’re sharing it)

I forgot those existed. I was looking at the static constexpr variables, but noticed they were protected. I didn’t think to check for functions.

In which case it should be:

uint8_t calculateStringWidth(uint8_t length)
{
	constexpr uint8_t characterWidth = Arduboy2::getCharacterWidth();
	constexpr uint8_t characterSpace = Arduboy2::getCharacterSpacing();

	return ((length * characterWidth) + ((length - 1) * characterSpace));
}

I don’t know what you mean by this.

Don’t worry about erasing the player, just don’t draw it in the first place.

if(!collisionHappened)
	drawPlayer();

It’s almost always easier to not do something than it is to undo something that’s already been done.

  • countDigits counts the number of decimal digits that would be required to print the decimal representation of value. (Bearing in mind that these numbers are actually binary, not decimal - arduboy.print calculates and prints the decimal representation.)
  • calculateStringWidth calculates the width (in pixels) of a string containing the number of characters specified. By feeding the result of countDigits into that, you get the width (in pixels) that the decimal representation of value would occupy on the screen.
  • calculateCentredX takes a width (in pixels) of something and calculates the X coordinate at which you would have to draw that something for it to be centred. By feeding it the result from above, you get the X coordinate at which value would have to be printed for the result to be centred on the screen.

In other words if you were to use it like so:

uint8_t x = calculateCentredXForInteger(value);
arduboy.setCursor(x, whatever);
arduboy.print(value);

Then value would be printed in the centre of the screen, regardless of whether it’s a 1, 2 or 3 digit value.

(To handle int16_t/uint16_t, just change the definition of calculateCentredXForInteger to use int16_t/uint16_t instead, and make sure you’re using the appropriate definition of countDigits.)

Because step 1 of the algorithm is to count the number of decimal digits that value will have when printed.

  • digits is the number of decimal digits that value has
  • width is the width (in pixels) of the textual representation of value when it is printed to the screen.
  • x is the position that value would have to be printed at in order for its textual representation to be horizontally centred in the middle of the screen.

Each function does something small and specific, and you need all three to be used in cooperation to be able to print a centred integer.

That’s what it is doing. Read the variable names. The outpu (i.e. return value) of countDigits is stored into digits, which then becomes the input (i.e. argument) of calculateStringWidth, and the output of that (i.e. its return value) is stored into width.

I’ve broken it down into several statements using named variables instead of writing:

uint8_t calculateCentredXForInteger(uint8_t value)
{
	return calculateCentredX(calculateStringWidth(countDigits(value)));
}

Because chaining all those functions together looks quite ugly.

You can include the Arduboy2 library as often as you need.

You don’t need an arduboy object to access every function because some functions are static, which means you only need to do Arduboy2:: instead of arduboy., and even if you did you could just extern it in a header and define it once in a .cpp file somewhere.


reinterpret_cast is safer than a C-style cast because you can’t accidentally cast away the const (which is a bad thing to do).

(Technically static_cast would have worked here too, and done more or less the same thing since pgm_read_ptr returns a void *.)

Relevant reading:

Technically you could do it using only C-style casts:

const char * string = (const char *)(pgm_read_ptr(&badWords[endWord]));
size_t halfWordWidth = (((strlen_P(string) * 12) + 1) / 2);
arduboy.setCursor(64 - (int)(halfWordWidth), 25);
arduboy.setTextSize(2);
arduboy.print((const __FlashStringHelper *)(string));

But that’s a lot messier and riskier.

More so because the inefficiency (and risk) of using memcpy_P and a char buffer makes me frown.

Sure, the more efficient version is ugly too, but that’s Arduino’s fault for sticking low-level. Given half a chance it could be turned into:

const char * string = progmemRead<const char *>(&badWords[endWord]);
size_t halfWordWidth = (((progmemStringLength(string) * 12) + 1) / 2);
arduboy.setCursor(64 - (int)(halfWordWidth), 25);
arduboy.setTextSize(2);
arduboy.print(flashStringHelper(string));

Or better yet:

ProgmemString string { progmemRead<const char *>(&badWords[endWord]) };
size_t halfWordWidth = (((string.size() * 12) + 1) / 2);
arduboy.setCursor(64 - (int)(halfWordWidth), 25);
arduboy.setTextSize(2);
arduboy.print(string);

(For no extra cost.)

2 Likes

For example, if you had a white object “behind” (or really at the same spot) the main white object, then turned the object black, there would be a “hole” in the other white object behind.

Oh, I didn’t realize you could do this!

After a bit more work, everything seems to be working, thanks!

1 Like

Yes, there would be a ‘hole’.

That’s because you aren’t actually turning the object black, you’re actually painting over top of the other object.

You can’t erase something after it’s been drawn because drawing (in the way it’s normally done on computers) is an inherantly destructive process.

All this talk of ‘painting’ and ‘drawing’ is actually metaphorical. What you’re really doing with ‘drawing’ functions on a computer is changing the values of some bits that represent a black and white image (0 representing black, 1 representing white).

It’s not like drawing or painting in real life, there’s no layers of material and no rubber to wear away the graphite or paint thinner to peel back the layers of paint.

The frame buffer doesn’t track everything that’s been drawn, it only tracks the current values of the pixels. If you change a pixel it stays changed, and any information about its previous value is lost.

All you can do to avoid creating a ‘hole’ in the object behind is to choose to not draw the object above. That’s actually how transparency works too - transparent pixels are simply an indication to the drawing algorithm leave that particular pixel unaltered in the source image.

That’s also why you should process all logic before attempting to draw anything: you need to decide whether or not something is going to be drawn and what colour/sprite will be used to represent it before you start drawing a frame, because you can’t just undo something you’ve drawn.

Lastly, to point it out: Sprites::drawErase doesn’t actually ‘erase’ a sprite in the way you’re thinking it does. Essentially if you had a fully white frame buffer, drawErase would draw your sprite in black and leave the white untouched. It’s effectively the equivalent of Sprites::drawSelfMasked for a source image that’s all white rather than all black.

1 Like

Yep, I know. I think I remember reading about that in the Arduboy2 Class Reference.