Filling a black rect on an inverted display

I'm running the latest sdk on linux and having an issue where I can't fill a rect properly. I set pd.display.setInverted(true), so everything is inverted.

In order to display white UI text over a filled black rect, I'm trying:

function createBackground(x, y, width, height)
    backgroundSprite = gfx.sprite.new()

    local backgroundImage = gfx.image.new(width, height)
    gfx.pushContext(backgroundImage)
    gfx.setColor(gfx.kColorWhite)
    gfx.fillRect(0, 0, width, height)
    gfx.popContext()
    backgroundSprite:setImage(backgroundImage)

    backgroundSprite:setCenter(0, 0)
    backgroundSprite:moveTo(x, y)
    backgroundSprite:add()
end

However, I don't see anything. If I update the code to gfx.setColor(gfx.kColorBlack), I get a white filled rect over my black background, but I want a black filled rect over a black background for drawing UI elements on top of.

I'm calling the above code from this code:

function createScoreDisplay(x, y)
    scoreSprite = gfx.sprite.new()
    updateScoreDisplay()
    scoreSprite:setCenter(0, 0)
    scoreSprite:moveTo(x, y)
    scoreSpriteWidth, scoreSpriteHeight = scoreSprite:getSize()
    createBackground(x, y, scoreSpriteWidth, scoreSpriteHeight)
    scoreSprite:add()
end

function updateScoreDisplay()
    gfx.setFont(uiFont)
    local scoreText = "Score: " .. score
    local textWidth, textHeight = gfx.getTextSize(scoreText)
    local scoreImage = gfx.image.new(textWidth + (uiOffsetX * 2), textHeight + (uiOffsetY * 2))
    gfx.pushContext(scoreImage)
    gfx.drawText(scoreText, uiOffsetX, uiOffsetY)
    gfx.popContext()
    scoreSprite:setImage(scoreImage)
end

Any ideas why this isn't working?

1 Like

It sounds like the filled sprite is rendering at least, so have you changed the drawing mode? Mode kDrawModeWhiteTransparent will render black, but white will be transparent. Meanwhile it won’t affect things like fillRect that use setColor instead.

Sprites have their own individual drawing modes.

I tried those out but still no luck. Although I think your last sentence might be pointing out my issue. Drawing modes don't affect fill rect, so I'm still unable to fill a black rect on a black background (while inverted, so it's set to white I suppose). It keeps coming out transparent.

Is there a better way to go about writing a score UI at the top of the screen that doesn't leak the images below it through?

It really depends on the whole context of your game I guess. I know sprites are often used for that, but for my own needs I haven only needed to draw UI directly to the screen in one region, and draw the game in another, without overlap.

(setColor is for filled shapes, so drawing mode is not relevant to them. Drawing mode is relevant for the resulting sprite, but filling a rectangle with color is done just the way you already are.)

I think it must be that the state of your graphics context is not as you think it should be. Some colour or mode or invert is wrong. (Or maybe the sprite is not being marked as dirty in some case?)

You can wrap your use of colours and modes in pushContext(image) to help prevent the general context becoming dirty or in an "unknown" or "unpredictable" state.

I would use either spriteBackground, or a separate sprite that you position under the score, or simply make the score sprite wider and position the score within that area.

I've reconstructed a full working example based on your functions

rect.zip (15.5 KB) source + pdx

image

import "CoreLibs/sprites"

local gfx = playdate.graphics

playdate.display.setInverted(true)

score = 1234
uiOffsetX = 10
uiOffsetY = 5

// i do this trick when running inverted to make sense of which colour is which!
BG = gfx.kColorWhite
FG = gfx.kColorBlack

function createBackground(x, y, width, height)
    backgroundSprite = gfx.sprite.new()

    local backgroundImage = gfx.image.new(width, height)
    gfx.pushContext(backgroundImage)
    gfx.setColor(BG)
    gfx.fillRect(0, 0, width, height)
    gfx.popContext()
    backgroundSprite:setImage(backgroundImage)

    backgroundSprite:setCenter(0, 0)
    backgroundSprite:moveTo(x, y)
    backgroundSprite:add()
end

function createScoreDisplay(x, y)
    scoreSprite = gfx.sprite.new()
    updateScoreDisplay()
    scoreSprite:setCenter(0, 0)
    scoreSprite:moveTo(x, y)
    scoreSpriteWidth, scoreSpriteHeight = scoreSprite:getSize()
    createBackground(x, y, scoreSpriteWidth, scoreSpriteHeight)
    scoreSprite:add()
end

function updateScoreDisplay()
    gfx.setFont(uiFont)
    local scoreText = "Score: " .. score
    local textWidth, textHeight = gfx.getTextSize(scoreText)
    local scoreImage = gfx.image.new(textWidth + (uiOffsetX * 2), textHeight + (uiOffsetY * 2))
    gfx.pushContext(scoreImage)
    gfx.drawText(scoreText, uiOffsetX, uiOffsetY)
    gfx.popContext()
    scoreSprite:setImage(scoreImage)
end

createScoreDisplay(150,0)

local backgroundImage = gfx.image.new(400,250)
gfx.pushContext(backgroundImage)
gfx.setDitherPattern(0.8, gfx.image.kDitherTypeBayer8x8)
gfx.fillRect(0,0,400,240)
gfx.setColor(BG)
gfx.fillRect(0,0,400,scoreSprite.height)
gfx.popContext()

gfx.sprite.setBackgroundDrawingCallback(
    function( x, y, width, height )
        -- x,y,width,height is the updated area in sprite-local coordinates
        -- The clip rect is already set to this area, so we don't need to set it ourselves
        backgroundImage:draw( 0, 0 )
    end
)

function playdate.update()
    gfx.sprite.update()
end

Thank you so much for the example! Unfortunately, I still haven't been able to make it work, so I think you're right about something breaking it elsewhere in my code. This all started breaking when I implemented a camera and began redrawing the UI in a relative position of the map size on the screen rather than in a stationary coordinate on the screen size. I'm learning as I go, so I think my implementation is a bit janky. Not to mention the UI shakes as the player moves because it doesn't match the player's velocity!

I'm gonna have to devote some time to reworking my setup now that I have a camera involved and will report back if I can figure it out.

Ok revisiting this and going off your example, it looks like whatever is on the screen is getting drawn over it when it's black, but not when it's white, which is really odd.

Take this example:

function updateScoreDisplay()
    gfx.setFont(uiFont)
    local scoreText = "Score: " .. score
    local textWidth, textHeight = gfx.getTextSize(scoreText)
    local scoreImage = gfx.image.new(textWidth + (uiOffsetX * 2), textHeight + (uiOffsetY * 2))
    gfx.pushContext(scoreImage)
    -- background
    gfx.setColor(gfx.kColorWhite)
    gfx.fillRect(0, 0, textWidth, textHeight)
    -- score text
    gfx.setColor(gfx.kColorBlack)
    gfx.drawText(scoreText, uiOffsetX, uiOffsetY)
    gfx.popContext()
    scoreSprite:setImage(scoreImage)
end

I can read score, but items on my map overlap with the word as I move around.

Now here's another example with the code flipped around:

function updateScoreDisplay()
    gfx.setFont(uiFont)
    local scoreText = "Score: " .. score
    local textWidth, textHeight = gfx.getTextSize(scoreText)
    local scoreImage = gfx.image.new(textWidth + (uiOffsetX * 2), textHeight + (uiOffsetY * 2))
    gfx.pushContext(scoreImage)
    -- score text
    gfx.setColor(gfx.kColorBlack)
    gfx.drawText(scoreText, uiOffsetX, uiOffsetY)
    -- background
    gfx.setColor(gfx.kColorWhite)
    gfx.fillRect(0, 0, textWidth, textHeight)
    gfx.popContext()
    scoreSprite:setImage(scoreImage)
end

Now the score text is covered up by a black box, but the sprites on the screen still render over the black box itself.

So it appears that my first method might be working partially correctly but for some reason the black background becomes transparent. I need a way to make the score sprite be drawn absolutely above everything else.

As a side note, changing the color between white and black is not changing the score text color. It always draws as white. Something weird is definitely going on.

If you can provide a reduced case runnable example I'll tackle this for you

A few pointers:

setColor() doesn't affect text because fonts are actually images. You have to use setImageDrawMode().

setZIndex() can be used to reorder sprites. A higher z order sprite is drawn on top of lower z order sprites.

updateScoreDisplay generates a lot of garbage as you have the image.new there. It can have a big impact on performance if you call the function often.

Thanks both of you! Getting back around to looking at this project today and will try to get a minimum build or use @outgunned's advice and see what I can do.

That updateScoreDisplay point is a great tip. I've been dealing with a few performance issues on device, so that's very helpful to know.

Ok, using @matt's technique, I've drawn a black bar across the top of the screen. I then updated my score from an image to drawing text thanks to @outgunned's suggestion. Things are working a lot better, particularly now that I'm setting a Z index.

So now the top of the screen blocks out sprites perfectly. The only issue that remains is my tilemap is still leaking through the top menu bar. I don't see a way to set the z index on tilemaps. Any thoughts on making it not render on the top 24 pixels of the display?

To render my 240x400 spritesheet on a 600x360 map, I'm currently calling drawBackground() in the code below during my primary update() loop:

local pd <const> = playdate
local gfx <const> = pd.graphics

local spritesheet = gfx.imagetable.new("images/grass")
local backgroundTilemap

function createBackground()
    backgroundTilemap = gfx.tilemap.new()
    backgroundTilemap:setImageTable(spritesheet)
    backgroundTilemap:setSize(mapWidth, mapHeight)
    backgroundTilemap:setTiles(
        { 1, 1 },
        800)
end

function drawBackground()
    if backgroundTilemap then
        if camera then
            backgroundTilemap:draw(0, 0)
            backgroundTilemap:draw(0, 240)
        end
    end
end

What technically works is setting the tilemap to render over what the camera is currently displaying on the screen with a 24 pixel vertical offset like so:

local pd <const> = playdate
local gfx <const> = pd.graphics

local spritesheet = gfx.imagetable.new("images/grass")
local backgroundTilemap

function createBackground()
    backgroundTilemap = gfx.tilemap.new()
    backgroundTilemap:setImageTable(spritesheet)
    backgroundTilemap:setSize(mapWidth, mapHeight)
    backgroundTilemap:setTiles(
        { 1 },
        400)
end

function drawBackground()
    if backgroundTilemap then
        if camera then
            backgroundTilemap:draw(camera:left(), camera:top() + 24)
        end
    end
end

However, this causes the background to move around with the camera, which is nauseating.

Screenshot or Animation from the Simulator would help analysis. :sunglasses:

top-menu-bar-cutoff

top-cutoff-2

This is using the first block of code from my previous comment. The entire top of the screen has a black bar across it. You can see the zombie sprites are hidden behind it. The tilemap that uses a spritesheet is not. This is noticeably problematic in the top left where the spritesheet overlaps with the word "Score". It's easiest to see in the letter "o". But ideally I want zero sprites showing up at the top 24 pixels of the entire width of the screen.

I would try setting/forcing a dirty rect at the size of your black bar to force that area to redraw.

But I'll have to give this more thought. I've never used tile maps :grimacing:

I’ll try that tomorrow thanks. I just realized you made yoyozo, which I’m a big fan of. How did you draw the background on that if you aren’t using tilemaps? This is all new to me, so I’m not married to my approach.

Thanks!

I just keep track of a bunch of coordinates in a table, move them at different speeds, loop through them to draw them every frame. A very old school approach.

Might be worth checking out as well. If I switch my grass to individual sprites, it of course solves the issue with bleeding through the top black bar, but it's a performance nightmare on the device (drops to 5 fps). So tilemaps were the first thing I tried.

Ok I solved my problem using these two resources:

The gist of it was that I needed to create a tilemap from an imagetable and then convert the tilemap to a sprite, which can then have the z index set.

Thanks for all the guidance and help everyone!

3 Likes