Monochrome Gradient in Missile Mayhem
On the Playdate, each pixel is either on or off. This poses a challenge for graphical fades because there is no alpha channel, and there aren’t even any greys (👽!). We needed to fade from white to black for dissipating explosions, and I think the solution we ended up with works pretty well. We liked it enough that to use it not just for the explosions, but for the laser shot and screen transitions as well.

TLDR: If you just want the the final values, check below for the end result.
Using LCDPattern
LCDPattern
is basically an 8x8 paintbrush
for the Playdate API, and quickly flipping between patterns
is how I drew the fade-ins and fade-outs. Specifically,
LCDPattern
represents an 8x8 block of pixels.
Each LCDPattern
is 16 bytes. One byte
represents 8 horizontal pixels, so the first 8 bytes
represent the 8x8 block of pixels. The remaining 8 bytes
represent the transparency of the same pixels. The
background of the game is black, so I typically wanted these
to repeat each other so that a 1 bit would be drawn white
and a 0 bit would be transparent.
The LCDPattern
can be plugged right into the
normal draw calls in place of an LCDColor
, and
the pattern will be drawn offset from the upper left corner
of the screen. I made a small macro for constructing pattern
data, and I could do this:
#define LCDRepeatPattern(r0,r1,r2,r3,r4,r5,r6,r7) { \
(r0),(r1),(r2),(r3),(r4),(r5),(r6),(r7), \
(r0),(r1),(r2),(r3),(r4),(r5),(r6),(r7) \
}
const LCDPattern CHECKER_PATTERN = LCDRepeatPattern(
0b01010101,
0b10101010,
0b01010101,
0b10101010,
0b01010101,
0b10101010,
0b01010101,
0b10101010);
// Then later...
->graphics->drawRect(x, y, w, h, CHECKER_PATTERN); pd
Which pattern?
I initially tried my hand at manually creating a series
of LCDPatterns
, but the results were not good.
Parts of the fade would be too slow and other parts too
fast. Or maybe there just weren’t enough steps in
between.
So I searched the internet and found the excellent Ditherpunk article, which I have since seen mentioned in several Playdate discussions. The article was inspired in part by the dithering magic of Return of the Obra Din, and it provides several better looking black-white dithered gradients. Below are a couple of samples, and I chose the linear RGB because I felt the sRGB version stayed white for too long.


Using the pattern
How to turn the lower half of a PNG into C arrays? Mostly by using bash and Image Magick. These were the steps:
# Setup
input_image="$1"
output_prefix="patch"
y_start=21
y_end=28
chunk_size=8
width=$(magick identify -format "%w" "$input_image")
num_chunks=$((width / chunk_size))
echo "Cutting ${num_chunks} patches from width=${width}"
# Generate many small files
for ((i=0; i<num_chunks; i++)); do
x_start=$((i * chunk_size))
x_end=$((x_start + chunk_size - 1))
output_file=$(printf "${output_prefix}_%03d.png" $((i + 1)))
magick "$input_image" -crop "${chunk_size}x${chunk_size}+${x_start}+${y_start}" "$output_file"
echo " saved $output_file"
done
echo "Finished cutting patches."
- Next remove duplicates. Duplicate images will always be next to each other, so just doing a dumb NxN comparison works:
files=($(find "." -name "patch_*.png" -type f))
compare_images() {
difference=$(magick compare -metric AE "$1" "$2" null: 2>&1)
[ "$difference" -eq 0 ]
}
for ((i=0; i<${#files[@]}; i++)); do
for ((j=i+1; j<${#files[@]}; j++)); do
if [ -f "${files[i]}" ] && [ -f "${files[j]}" ]; then
if compare_images "${files[i]}" "${files[j]}"; then
echo "Duplicate found: ${files[j]} matches ${files[i]}"
echo "Removing: ${files[j]}"
rm "${files[j]}"
fi
fi
done
done
echo "Finished removing duplicates."
I got 13 removals, and that left me with these 32 patches:

- Convert the remaining 8x8 patch PNGs
into text. For this, you can use Image Magick’s text output,
then
sed
#FFF to 1 and #000 to 0. The Image Magick to text representation is interesting to me:
λ magick "patch_15.png" +repage -crop "8x1+0+0" +repage txt:-
# ImageMagick pixel enumeration: 8,1,0,255,gray
0,0: (0) #000000 gray(0)
1,0: (0) #000000 gray(0)
2,0: (255) #FFFFFF gray(255)
3,0: (0) #000000 gray(0)
4,0: (0) #000000 gray(0)
5,0: (0) #000000 gray(0)
6,0: (255) #FFFFFF gray(255)
7,0: (0) #000000 gray(0)
So using that output, this script will generate a C file
with the LCDPattern
objects:
# Clear output file
output_file="patch_bitfields.c"
> "$output_file"
for png_file in patch_*.png; do
patch_number=$(echo "$png_file" | sed 's/patch_\([0-9]*\)\.png/\1/')
echo "Processing $png_file (Patch $patch_number)"
# Start of C output
echo "static LCDPattern patch_$patch_number = LCDRepeatPattern(" >> "$output_file"
# Row by row
for ((row=0; row<8; row++)); do
pixel_data=$(magick "$png_file" +repage -crop "8x1+0+$row" +repage txt:-)
binary_row=$(echo "$pixel_data" | tail -n +2 | awk '{print $3}' | sed 's/#FFFFFF/1/g; s/#000000/0/g' | tr -d '\n')
if [ $row -lt 7 ]; then
echo " 0b$binary_row, // Row $((row+1))" >> "$output_file"
else
echo " 0b$binary_row); // Row $((row+1))" >> "$output_file"
fi
done
# Add newline after each object
echo "" >> "$output_file"
done
echo "Finished png -> txt conversion. Results written to $output_file"
This yielded a bunch of individual objects that look like this:
static LCDPattern patch_15 = LCDOpaquePattern(
0b00100010, // Row 1
0b01010101, // Row 2
0b00001000, // Row 3
0b01010101, // Row 4
0b00100010, // Row 5
0b01010101, // Row 6
0b00000000, // Row 7
0b01010101); // Row 8
Then I used the editor to rearrange these 32 objects into
an array, and I manually added a pattern of all
0b11111111
for a pure white step. So 33
patterns in all.
The final output
I like this in rows because it gives a nice Matrix digital rain effect!
#define GRADIENT_PATTERN_COUNT 33
const LCDPattern GRADIENT_PATTERN[GRADIENT_PATTERN_COUNT] = {
( 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111 ),
LCDRepeatPattern( 0b11111111, 0b11111111, 0b11111111, 0b01110111, 0b11111111, 0b11111111, 0b11111111, 0b01111111 ),
LCDRepeatPattern( 0b11111111, 0b11011111, 0b11111111, 0b01110111, 0b11111111, 0b11111111, 0b11111111, 0b01110111 ),
LCDRepeatPattern( 0b11111111, 0b01011111, 0b11111111, 0b01110111, 0b11111111, 0b11011101, 0b11111111, 0b01110111 ),
LCDRepeatPattern( 0b11111111, 0b01011101, 0b11111111, 0b01110111, 0b11111111, 0b01010101, 0b11111111, 0b01110111 ),
LCDRepeatPattern( 0b11111111, 0b01010101, 0b11111111, 0b01010111, 0b11111111, 0b01010101, 0b11111111, 0b01010111 ),
LCDRepeatPattern( 0b10111111, 0b01010101, 0b11111111, 0b01010101, 0b11111111, 0b01010101, 0b11111111, 0b01010101 ),
LCDRepeatPattern( 0b10111111, 0b01010101, 0b11111111, 0b01010101, 0b10111011, 0b01010101, 0b11111111, 0b01010101 ),
LCDRepeatPattern( 0b10111011, 0b01010101, 0b11101111, 0b01010101, 0b10111011, 0b01010101, 0b11111111, 0b01010101 ),
LCDRepeatPattern( 0b10111011, 0b01010101, 0b10101110, 0b01010101, 0b10111011, 0b01010101, 0b11101110, 0b01010101 ),
LCDRepeatPattern( 0b10111011, 0b01010101, 0b10101110, 0b01010101, 0b10111011, 0b01010101, 0b10101010, 0b01010101 ),
LCDRepeatPattern( 0b10101011, 0b01010101, 0b10101010, 0b01010101, 0b10111011, 0b01010101, 0b10101010, 0b01010101 ),
LCDRepeatPattern( 0b10101011, 0b01010101, 0b10101010, 0b01010101, 0b10101010, 0b01010101, 0b10101010, 0b01010101 ),
LCDRepeatPattern( 0b00101010, 0b01010101, 0b10101010, 0b01010101, 0b00100010, 0b01010101, 0b10101010, 0b01010101 ),
LCDRepeatPattern( 0b00100010, 0b01010101, 0b10001010, 0b01010101, 0b00100010, 0b01010101, 0b10101010, 0b01010101 ),
LCDRepeatPattern( 0b00100010, 0b01010101, 0b10001010, 0b01010101, 0b00100010, 0b01010101, 0b10001000, 0b01010101 ),
LCDRepeatPattern( 0b00100010, 0b01010101, 0b00001000, 0b01010101, 0b00100010, 0b01010101, 0b10001000, 0b01010101 ),
LCDRepeatPattern( 0b00100010, 0b01010101, 0b00001000, 0b01010101, 0b00100010, 0b01010101, 0b00000000, 0b01010101 ),
LCDRepeatPattern( 0b00000010, 0b01010101, 0b00000000, 0b01010101, 0b00100010, 0b01010101, 0b00000000, 0b01010101 ),
LCDRepeatPattern( 0b00000000, 0b01010101, 0b00000000, 0b01010101, 0b00000000, 0b01010101, 0b00000000, 0b01010101 ),
LCDRepeatPattern( 0b00000000, 0b01010101, 0b00000000, 0b01010101, 0b00000000, 0b01010101, 0b00000000, 0b00010101 ),
LCDRepeatPattern( 0b00000000, 0b01010101, 0b00000000, 0b00010001, 0b00000000, 0b01010101, 0b00000000, 0b00010101 ),
LCDRepeatPattern( 0b00000000, 0b01010101, 0b00000000, 0b00010001, 0b00000000, 0b01010101, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b01000101, 0b00000000, 0b00010001, 0b00000000, 0b01010101, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b01000101, 0b00000000, 0b00010001, 0b00000000, 0b01000100, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b00000100, 0b00000000, 0b00010001, 0b00000000, 0b01000100, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b00000100, 0b00000000, 0b00010001, 0b00000000, 0b01000000, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b00000100, 0b00000000, 0b00010001, 0b00000000, 0b00000000, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b00000000, 0b00000000, 0b00010001, 0b00000000, 0b00000000, 0b00000000, 0b00010001 ),
LCDRepeatPattern( 0b00000000, 0b00000000, 0b00000000, 0b00010001, 0b00000000, 0b00000000, 0b00000000, 0b00000001 ),
LCDRepeatPattern( 0b00000000, 0b00000000, 0b00000000, 0b00010000, 0b00000000, 0b00000000, 0b00000000, 0b00000001 ),
LCDRepeatPattern( 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000001 ),
LCDRepeatPattern( 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 )
LCDRepeatPattern};
And the result of stepping through the
LCDPattern
arrays is explosions like this:
