Something that I ran into while building out our Prismatic Engine was a lack of good documentation around LCDPatterns
in the Playdate C API.
There are some forum posts that were super helpful in understanding how patterns are supposed to work and, of course, the people on the dev forums are always helpful. In general though, the documentation leaves something to be desired.
In general, the C documentation assumes a certain level of understanding which I think is fine: You would probably just stick to building with Lua or Pulp if ease-of-use was your primary goal. But there are parts of it that could be clearer or expanded further.
With that mini-rant out of the way, I wanted to document my understanding of how this all works to add to the pool of information that is available for people searching this up in the future.
I’ll start with my context: I wanted to build a custom transition system for my game engine. My goal was to be able to take any LCDBitmap
of any size and apply an image effect to it in an update loop, for as long as the user specifies. The C API does not provide the pre-built image effects that the Lua API does, so I ended up needing to roll my own effects.
I will spare you the transition engine details since the focus of this post is mainly to describe what I learned about working with LCDPattern
s. I’ll be specifically focusing on the effects here.
What we know from the documentation is that LCDPattern
is an alias for uint8_t[16]
. A uint8_t
is an 8-bit unsigned integer or, in other words, a value from 0-255. Since it’s an 8-bit int, we can easily represent its values with hex (0x00-0xFF) or binary (b00000000-b11111111). Thinking of the patterns in this way almost visually represents what you will see in the pattern, and makes it easier to conceptualize how math will affect the rendering.
The way that the pattern works is that the first 8 elements in the array represent the pattern bitmap, and the last 8 elements in the array represent “opacity”. Since the playdate is 1-bit, it’s not true opacity, but you can think of it in a similar way. It is basically an additional bitmap mask that is applied over the pattern.
So, to define a pattern where the image being masked is fully visible, we’d set all elements to 255, 0xFF, or b11111111.
LCDPattern pattern = {
// bitmap
0b11111111,0b11111111,0b11111111,0b11111111,
0b11111111,0b11111111,0b11111111,0b11111111,
// mask
0b11111111,0b11111111,0b11111111,0b11111111,
0b11111111,0b11111111,0b11111111,0b11111111,
};
And conversely, to define a fully invisible image we’d leave the mask bits high (255) and set the bitmap bits low (0).
LCDPattern pattern = {
// bitmap
0b00000000,0b00000000,b000000000,0b00000000,
0b00000000,0b00000000,0b00000000,0b00000000,
// mask
0b11111111,0b11111111,0b11111111,0b11111111,
0b11111111,0b11111111,0b11111111,0b11111111,
};
Each bit in the first 8 bytes represents an on or off pixel, and each byte a row of pixels in the resulting bitmap image. The pattern automatically repeats to fill out the space of the LCDBitmap that it is applied to.
Patterns on their own are interesting, but they don’t provide much more than that. What starts to make things interesting is that you can cast an LCDPattern
to an LCDColor
. Admittedly the way that this is done is a little bit hacky but, as far as I’m aware, this is how it’s meant to be used.
If you consider that you can cast a pattern to a color, this opens up all of the functions that work with LCDColor
s in the documentation for use with your pattern. Now we’re getting somewhere.
To demonstrate this point, here’s a snippet from our transition engine:
int w = 0, h = 0;
playdate->graphics->getBitmapData( transition->image, &w, &h, NULL, NULL, NULL );
transition->_rendered = playdate->graphics->copyBitmap( transition->image );
transition->mask = playdate->graphics->newBitmap( w, h, (LCDColor)transition->pattern );
playdate->graphics->setBitmapMask( transition->_rendered, transition->mask );
playdate->graphics->drawBitmap( transition->_rendered , transition->x, transition->y, transition->flipped )
There is obviously some context missing and, if you are interested in the specific details, I encourage you to check out this part of the engine code. But the short version of what’s happening here is this:
- We take an existing image, stored on the transition in this case, and get its width and height
- We make a copy of the existing image on the fly
- We create a new image, exactly the same size as the original image, based on an
LCDPattern
- We set our pattern image as a bitmap mask for the copy of the original
- We draw the copy to the screen
The engine is doing this all in a loop while transforming the pattern, which is what creates an animated transition effect. But even without that added complexity, you could still use this method to create a static image with a patterned mask over it.
Combining this concept with the pattern logic above, you can experiment with setting different values for the first 8 bytes in the LCDPattern. When applied as a mask over another LCDBitmap, you’ll see the pattern affects the image in interesting ways.
For my purposes, leaving the pattern mask bits set high was what made the most sense, but you can also manipulate those bits to create even more interesting patterns.
If you are interested in digging even deeper into this topic and learning about animating patterns, I wrote up an explanation in the engine documentation for users interested in creating custom effects. It explains the concepts in the context of our engine, but the core concepts will be the same regardless of how you choose to approach the animation.