playdate.datastore.writeImage Transparency

I am doing something a bit unconventional, I think, and am running into an issue that I cannot seem to solve. I've created a little library to use the Playdate simulator to act as a "rendering engine" to create animations for myself to reuse in my Playdate games. This allows me to, among other things, pre-render particles and sprite rotations.

The way I've gone about this is by capturing the contents of the screen over multiple update cycles using playdate.graphics.getWorkingImage. I draw these screenshot onto new images of a certain size in order to crop them (using lockFocus). Then, I write these cropped screenshots to my hard disk via playdate.datastore.writeImage.

This frees up essential CPU cycles during gameplay by allowing me to use these images as imageTable animations, rather than executing the actual particle simulation.

The problem, though, is that I cannot get transparency to work. It seems from the documentation for playdate.datastore.writeImage that GIF transparency is not supported. So I attempted to use the PDI format instead... but transparency is either not supported for this filetype or I am doing something wrong.

Any idea if PDI should support transparency in this case?

Any word on if GIF transparency will be supported sometime soon? Is it on the roadmap?

Are there any workarounds that come to mind - or ways to read in a file via playdate.datastore.readImage that have transparent pixels? (Either PDI or GIF)

did you try drawing to an (initially transparent) image and saving that to disk? Rather than getWorkingImage?

Otherwise, it is possible to add transparency using imagemagick in post-process, on the command line, but is suited only for contiguous areas. This is what I did for the Sparrow Solitaire launcher animation.

I'm interested in the solution for this because my animation tool would benefit from it.

1 Like

Might be worth an SDK feature request? It would be a useful addition.

I think you could roll your own gif writer (might be some help here?) but it would be easier if the SDK function could handle it.

1 Like

datastore.writeImage() does preserve transparency, both in pdi format and (as of 1.10) in gif. We don't have a gif decoder in the firmware though (surprisingly, it's much easier to implement a gif encoder than a decoder) so you'd have to use the default pdi format here, by not adding a gif extension to the file name.

If I'm understanding it correctly, the issue here is that the framebuffer images (working image and display image) don't have transparency; all drawing is composited onto an opaque background. To get a transparent background you'd need to render into a transparent image (i.e., created with background color kColorClear) using lockFocus(), like:

for angle=10,350,10
    local buf = gfx.image.new(width, height, gfx.kColorClear)
    gfx.lockFocus(buf)
    source:drawRotated(angle, width/2, height/2)
    gfx.unlockFocus()
    playdate.datastore.writeImage(buf, "rotated-"..tostring(angle))
end
2 Likes

Matt, thanks for the suggestion - I did write up a bash script to use imagemagick as a workaround & it functioned pretty well at removing the backgrounds in my GIFs. Here's the actual imagemagic command for anyone interested:

magick input.gif -fuzz 5% -bordercolor white -border 1 -fill none -draw "alpha 0,0 floodfill" -shave 1x1 result.gif;

The main drawback of this, however, is that certain rotations of the sprite would break the outline & cause imagemagick to steal some of the inner-fill. Could have been solved with revisiting the input images & adding a thicker outline or cleaning up the resulting images.

However, Daves suggestion worked wonders. At some point during testing, I was clearing the background with kColorClear but I think it was still capturing a solid background when writing the working image to disk. That was unexpected.

Dave's buffer approach is much more simple and clean, overall. I ended up generalizing it a bit, if anyone is interested in this sort of functionality:

function captureRender(_drawFn,_name,_duration,_width,_height,_useGif)
    local extension=_useGif and ".gif" or ""
    local timestamp=playdate.getSecondsSinceEpoch()
    local canvas=playdate.graphics.image.new(_width, _height, playdate.graphics.kColorClear)

    for i = 1, _duration do
        playdate.graphics.lockFocus(canvas)
            _drawFn()
        playdate.graphics.unlockFocus()

        playdate.datastore.writeImage(canvas, './ssir-render/'.._name.."/"..timestamp.."/".._name.."-table-"..i..extension)
    end
end

I plan to pre-render everything in a side-project and hand-pick which animations to use in my main game. However, with this function, you could actually execute it in your game's init & render everything needed on boot. This would probably be fine for the rotation sort of use-case. Although, if you are simulating elements (like particles), then every run of your game would have it's own unique animations, for that instance only... which is kind of an interesting concept. Certainly not widely applicable, but maybe there's something there.

Anyways, thanks so much everyone


PS - for posterity - The initial error I was receiving on build was:

error: DGifSlurp() failed: 107

Hopefully this will help anyone searching through the forums in the future.

1 Like

I also suggested it before imagemagick :wink:

Dave's code snippet went the extra mile though!

2 Likes

:man_facepalming: Not sure how I missed that! It was a long day lol

1 Like