Bug in fast_fade example (possible bad semantics of lock/unlockFocus)

The fast_fade example in 'Single File Examples' has a bug in it. It SHOULD show a black "ONE" on a white background fading out as a white "TWO" on a black background fades in. However if you run the example the "ONE" isn't actually shown at all, only the "TWO" fades.

The bug is subtle. In the playdate.update() function the focus is locked to the frontImage mask which is then cleared to black (making frontImage transparent). Then patternWithOpacity() is called to get a pattern which is then tiled into the mask and the focus unlocked. However patternWithOpacity() itself calls lock/unlockFocus() so when that returns the focus is actually unlocked. The pattern, instead of being written to the mask, is just written to the screen directly and then frontImage is drawn on top, however that's entirely transparent. The effect looks very similar, however 'ONE' is missing and what's actually happening is nothing to do with masks at all.

One fix is to push/pop the graphics context inside patternWithOpacity() which saves the graphics focus. Or to call patternWithOpacity before locking focus.

This bug does highlight a potentially bad semantic in lock/unlockFocus(). Any routine could call it to do drawing and not everyone will think about pushing/popping the graphics context, which may also be an expensive operation. I think either lock/UnlockFocus() should stack calls so that it really works more like push/popFocus() or calling lockFocus() when already locked, or unlockFocus() when unlocked should assert, perhaps with a message suggesting nested lock/unlockFocus calls need the graphics context to be pushed. Personally I'd prefer the stacked version where lockFocus(A)->lockFocus(B)->unlockFocus()->unlockFocus() switched from screen -> A -> B -> A -> screen which I think is what most people would expect to happen.

4 Likes

For those reading this and wondering why the example is not working as expected, please find an updated version as per @rols 's comment:

--
-- Demonstrating of a way to fade between images using a pattern for good on-device performace
--

import 'CoreLibs/graphics.lua'

local gfx = playdate.graphics
playdate.display.setRefreshRate(50)

fnt =  gfx.getSystemFont()
gfx.setFont(fnt)

-- Create a White Image with an alpha mask
local frontImg = gfx.image.new(400, 240, gfx.kColorWhite)
frontImg:addMask(0)
gfx.pushContext(frontImg)
gfx.drawTextAligned("*ONE*", 200, 100, kTextAlignment.center)
gfx.popContext()

-- Fill the alpha mask with white which makes it fully opaque
local mask = frontImg:getMaskImage()
gfx.pushContext(mask)
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(0, 0, 400, 240)
gfx.popContext()

-- Create a black background image
local backImg = gfx.image.new(400, 240, gfx.kColorBlack)
gfx.pushContext(backImg)
gfx.setImageDrawMode(gfx.kDrawModeInverted)
gfx.drawTextAligned("*TWO*", 200, 130, kTextAlignment.center)
gfx.popContext()

local lastTimeMilliseconds = 0
local ct = 0
local dt = 0

local FADE_TIME = 3000
local fade_timer = FADE_TIME
local pat
local alpha

-- Creates an 8x8 white image that's faded with the given alpha
local patternImg = gfx.image.new(8, 8)
local function patternWithOpacity(alpha)
    gfx.pushContext(patternImg)
    gfx.setColor(gfx.kColorWhite)
    gfx.fillRect(0, 0, 8, 8)
    gfx.popContext()
    return patternImg:fadedImage(alpha, gfx.image.kDitherTypeBayer8x8)
end

function playdate.update()

    ct = playdate.getCurrentTimeMilliseconds()
    if lastTimeMilliseconds > 0 then
        dt = ct - lastTimeMilliseconds
    end
    lastTimeMilliseconds = ct

    fade_timer = fade_timer - dt
    if fade_timer < 0 then fade_timer = 0 end

    backImg:draw(0, 0)

    if fade_timer > 0 then
        alpha = fade_timer / FADE_TIME

        -- draw the faded 8x8 image into the alpha mask. As more black pixels appear in the
        -- pattern the foreground image will appear to fade out

        gfx.pushContext(mask)
        gfx.setColor(gfx.kColorBlack)
        gfx.fillRect(0, 0, 400, 240)
        pat = patternWithOpacity(alpha)
        pat:drawTiled(0, 0, 400, 240)
        gfx.popContext()

        frontImg:draw(0, 0)

    else
        fade_timer = FADE_TIME
    end

    playdate.drawFPS(380, 0)
end

Output:
fade

1 Like