Outline around a group of sprites?

I want to represent a snake as a series of snake segments which each have their own properties. I've got this working by creating a 'Snake' class which contains a table of objects from the 'SnakeSegment' class.

To display the snake on screen I've made the SnakeSegment class an extension of the SDK's built-in sprite class: class('SnakeSegment').extends(playdate.graphics.sprite)

And to display the segment as a circle I've done the following during SnakeSegment's init:

local gfx <const> = playdate.graphics

function SnakeSegment:init()
    -- A bunch of initialization-type stuff, and then...

    self:moveTo(self.x, self.y)
    -- Define the bounding box
    local circleImage = gfx.image.new(self.radius*2, self.radius*2)
    gfx.pushContext(circleImage)
        -- Draw a black circle
        gfx.fillCircleAtPoint(self.radius, self.radius, self.radius)
    gfx.popContext()
    self:setImage(circleImage)
    self:add()  -- add to the display list
end

This works great and the snake is displayed correctly as a series of tightly-spaced black circles. The problem though is that the snake does not display well when rendered against a black background. The natural solution is to put a white outline around it. But if I simply add a white circle behind the black circle though I lose the continuous snake-like nature of the snake as it's interrupted by the outlines (quick example image below).

    -- This solution doesn't work very well:
    gfx.pushContext(circleImage)
        -- Draw white outline
        gfx.setColor(gfx.kColorWhite)
        gfx.fillCircleAtPoint(self.radius, self.radius, self.radius+outline_width)
        -- Draw black circle
        gfx.setColor(gfx.kColorBlack)
        gfx.fillCircleAtPoint(self.radius, self.radius, self.radius)
    gfx.popContext()

Example of the above solution which doesn't work very well:
image

I could abandon my attempt to extend the SDK's sprite class and simply write my own class to handle the segments, but I'm trying to learn to use the SDK to it's full potential (I'm new to the SDK). I'd prefer to solve this in a 'playdate-y' kinda way. What is your recommendation for a 'playdate-y' kinda way to handle this outline-of-multiple-sprites problem? Your advice is much appreciated!! thanks!!

Running on Linux (Ubuntu 22.04.1)

One way to do that is to draw all the outlines first then the insides. That requires two sprites per segment, with the fill on a higher z index:

COUNT=10
FILL_SIZE=20
OUTLINE_WIDTH=3

fills = {}
outlines = {}
gfx = playdate.graphics

function makeCircle(size, color)
	local img = gfx.image.new(size, size, gfx.kColorClear)
	gfx.lockFocus(img)
	gfx.setColor(color)
	gfx.fillEllipseInRect(0, 0, size, size)
	gfx.unlockFocus()
	return img
end

fillimg = makeCircle(FILL_SIZE, gfx.kColorWhite)
outlineimg = makeCircle(FILL_SIZE+2*OUTLINE_WIDTH, gfx.kColorBlack)

for i=1,COUNT do
	fills[i] = gfx.sprite.new()
	fills[i]:setImage(fillimg)
	fills[i]:setZIndex(10)
	fills[i]:add()
	
	outlines[i] = gfx.sprite.new()
	outlines[i]:setImage(outlineimg)
	outlines[i]:add()
end

t = 0
dt = 0.05

function playdate.update()
	for i=1,COUNT do
		local x = 200 + 100 * math.cos(t/4) * math.sin(t + i/6)
		local y = 120 + 100 * math.cos(t/10) * math.cos(t + i/6)
		fills[i]:moveTo(x,y)
		outlines[i]:moveTo(x,y)
	end
	gfx.sprite.update()
	t += dt
end

worm

7 Likes

This is a fantastic response!!! Super helpful!!! Thank you very much!!!

I think drawing a circle may be slower than drawing an image of a circle. So, if all circles are the same size, using an image might be an effective optimization

You might be able to do this when one sprite (of an outlined circle) by using different drawing moves to draw the inside of the outline.

But we're firmly in the optimisation weeds at this point!