Creating an image mask with sprites

I'm trying to figure out how to do the following:

  1. I want the entire screen to be black and obscured hiding sprites under it
  2. I want a circle to act as a mask so I can see through the black curtain in step 1 and see sprites behind it.

I also want flexibility for the circle to be scaled, moved, or modified overtime so I was hoping to use sprites I was creating dynamically. I've tried something like the following but I might not be understanding this correctly because it's not working.

spotlight = gfx.image.new(800, 480) -- it's twice the screen size because it needs to follow the character around the entire screen.
gfx.pushContext(spotlight)
        gfx.fillCircleAtPoint(400, 240, spotlightSize)
gfx.popContext()
spotlightSprite = gfx.sprite.new(spotlight)
spotlightSprite :moveTo(playerSprite.x, playerSprite.y)
spotlightSprite:add()

curtain = gfx.image.new(800, 480, gfx.kColorBlack)
curtain:setMaskImage(spotlight)
curtainSprite = gfx.sprite.new(curtain)
curtainSprite:setZIndex(10)
curtainSprite:add()

Setting the mask image separately seems a little odd.. I think it'd be easier to just fill the spotlight image black and then punch out the circle by drawing using kColorClear:

import "CoreLibs/graphics"
import "CoreLibs/sprites" -- for sprite.new(image) function

gfx = playdate.graphics

player = gfx.image.new(10,16)
gfx.pushContext(player)
gfx.drawText("@",0,0)
gfx.popContext()

playerSprite = gfx.sprite.new(player)
playerSprite:moveTo(200,120)
playerSprite:add()

spotlight = gfx.image.new(800, 480) -- it's twice the screen size because it needs to follow the character around the entire screen.

function drawSpotlight(size)
	gfx.pushContext(spotlight)
	gfx.setColor(gfx.kColorBlack)
	gfx.fillRect(0,0,spotlight:getSize())
	gfx.setColor(gfx.kColorClear)
	gfx.fillCircleAtPoint(400, 240, size)
	gfx.popContext()
end

drawSpotlight(100)
spotlightSprite = gfx.sprite.new(spotlight)
spotlightSprite:add()

function playdate.update()
	playerSprite:moveTo(playerSprite.x + math.random(-3,3), playerSprite.y + math.random(-3,3))
	spotlightSprite:moveTo(playerSprite.x, playerSprite.y)
	gfx.sprite.update()
end

One down side to using a full-screen image to blank most of the screen is you're doing a lot of unnecessary drawing, painting black over black. But convincing the system to only draw the center of the spotlight isn't easy at all. :confused:

yeah i ended up doing something like that too and looks like its working :slight_smile:
I wonder if there's a way to just not redraw the blacked out section every frame. And only redraw the spotlight area. I'll keep playing around with it. I appreciate the help!

I did come up with a way after posting the above (couldn't let it go..) but didn't want to make things even more confusing. But since you've got it working on your end, here's what I came up with:

import "CoreLibs/graphics"
import "CoreLibs/sprites" -- for sprite.new(image) function

gfx = playdate.graphics

player = gfx.image.new(10,16)
gfx.pushContext(player)
gfx.drawText("@",0,0)
gfx.popContext()

playerSprite = gfx.sprite.new(player)
playerSprite:moveTo(200,120)
playerSprite:setZIndex(1)
playerSprite:add()

function makeSpotlightImage(r)
	local image = gfx.image.new(2*r,2*r)
	gfx.pushContext(image)
	gfx.setColor(gfx.kColorBlack)
	gfx.fillRect(0,0,image:getSize())
	gfx.setColor(gfx.kColorClear)
	gfx.fillCircleAtPoint(r, r, r)
	gfx.popContext()
	return image
end

spotlightRadius = 50
spotlight = makeSpotlightImage(spotlightRadius)

spotlightSprite = gfx.sprite.new()
spotlightSprite:setSize(400,240)
spotlightSprite:moveTo(200,120)
spotlightSprite:add()

spotlightSprite.draw = function()
	local x,y = playerSprite.x, playerSprite.y
	
	-- fill in previous bounds
	gfx.setColor(gfx.kColorBlack)
	gfx.fillRect(0,0,x-spotlightRadius,240)
	gfx.fillRect(x+spotlightRadius,0,400-(x+spotlightRadius),240)
	gfx.fillRect(0,0,400,y-spotlightRadius)
	gfx.fillRect(0,y+spotlightRadius,400,240-(y+spotlightRadius))
	
	spotlight:draw(x-spotlightRadius,y-spotlightRadius)
end

gfx.sprite.addDirtyRect(0,0,400,240)

local lastx, lasty = 200, 120

function playdate.update()
	local x,y = playerSprite.x + math.random(-3,3), playerSprite.y + math.random(-3,3)
	playerSprite:moveTo(x,y)
	gfx.sprite.addDirtyRect(lastx-spotlightRadius, lasty-spotlightRadius, 2*spotlightRadius, 2*spotlightRadius)
	gfx.sprite.addDirtyRect(x-spotlightRadius, y-spotlightRadius, 2*spotlightRadius, 2*spotlightRadius)
	gfx.sprite.update()
	lastx, lasty = x, y
end

Adding the dirty rect by hand is kind of weird, I know. You could probably use an empty sprite to do the same thing. And you could also use gfx.sprite.setClipRect() to clip out all the scene drawing that should be covered by the spotlight effect.

1 Like

here was my first approach I mentioned above. I'll take a look at your snippet and see if I can understand it. I don't know how to easily measure perf impact but still in the early stages of playing around with this :slight_smile: Maybe it'd be something down the road I was glad I invested time in at the beginning!

import "CoreLibs/graphics"
import "CoreLibs/sprites"

local gfx <const> = playdate.graphics
local snd <const> = playdate.sound

local maskImage = gfx.image.new(400, 240)

spotlight = { 
	pos_x = 200,
	pos_y = 120,
	radius = 50,
}

local function init()
	-- placeholder for player
	player = gfx.image.new("")
	playerSprite = gfx.sprite.new(player)
end


function playdate.update()
	if playdate.buttonIsPressed(playdate.kButtonUp) then
		spotlight.pos_y -= 3
	elseif playdate.buttonIsPressed(playdate.kButtonDown) then
		spotlight.pos_y += 3
	end
	if playdate.buttonIsPressed(playdate.kButtonLeft) then
		spotlight.pos_x -= 3
	elseif playdate.buttonIsPressed(playdate.kButtonRight) then
		spotlight.pos_x += 3
	end
		
	gfx.sprite.update()
	gfx.lockFocus(maskImage) 
		-- cut out entire spotlight light
		gfx.setColor(gfx.kColorBlack)
		gfx.fillRect(0, 0, 400, 240)
		gfx.setColor(gfx.kColorClear)
		gfx.fillCircleAtPoint(spotlight.pos_x, spotlight.pos_y, spotlight.radius)
	gfx.unlockFocus()
	
	maskImage:draw(0,0)
end

init()