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.

Wave 5 tower fighting for his life.

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...
pd->graphics->drawRect(x, y, w, h, CHECKER_PATTERN);

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.

Black and white dither in sRGB
Black and white dither in linear RGB. This is the one I used.

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:

  1. Cut out the 8x8 patches
  2. Remove the duplicates
  3. Convert each patch file to a C array
  1. Cut out a bunch of 8x8 patches into different PNG files. Pretty straightforward:
# 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."
  1. 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:

All the patches together
  1. 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] = {
    LCDRepeatPattern( 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 )
};

And the result of stepping through the LCDPattern arrays is explosions like this:

In game explosion and fade