Animated dither patterns using phase offsets

Hey folks,

I've been getting my feet wet recently (on Mac) by exploring the sprite functionality. It's been great fun playing with the graphics APIs, and I've been surprised at how effective some of the dithering effects can be for creating simple graphics.

It would be slick if I could create some animated graphics by adjusting the phase offset when calling gfx.setDitherPattern before drawing in the same way you can when using dithering on images with gfx.image:drawBlurred. For instance, consider animating kDitherTypeDiagonalLine to create a conveyor belt effect.

Firstly, is there a way to do this that I'm missing? If not, is there a technical reason that such an approach would be inefficient or unwise? I realize I could achieve this using an imageTable animation loop, but I was hoping for a more lightweight approach. I almost posted this under feature requests, but that felt presumptuous since I'm new to the platform and don't really know my way around yet.

Thanks in advance for any tips!

Well, as usual, I should have played with this for a few more hours before posting. I think I've figured out the appropriate solution, but I'd welcome improvements. I wound up pushing a graphics context for a new image which I render the dither pattern into. I'm choosing a dither pattern based on the specified velocity, which impacts both pattern direction and the animation speed. Here's the setup:

    self.beltImage = gfx.image.new(16, 16) -- twice the size of the 8x8 pattern we'll extract

    gfx.pushContext(self.beltImage)
        gfx.setColor(gfx.kColorBlack)
        if self.velocity.x == 0 then
            gfx.setDitherPattern(0.5, gfx.image.kDitherTypeHorizontalLine)
        elseif self.velocity.y == 0 then
            gfx.setDitherPattern(0.5, gfx.image.kDitherTypeVerticalLine)
        else
            gfx.setDitherPattern(0.5, gfx.image.kDitherTypeDiagonalLine)
            self.diagonal = true
        end
        gfx.fillRect(0, 0, 16, 16)
    gfx.popContext()

This approach has the benefit of enabling rotation, which is needed to account for diagonal belts which run orthogonal to the diagonal dither pattern.

    -- rotate the image to acount for diagonals orthogonal to the dither pattern
    if (velocity.x < 0 and velocity.y > 0) or (velocity.x > 0 and velocity.y < 0) then
        self.beltImage = self.beltImage:rotatedImage(90)
    end

In playdate.update() I increment my X and Y offsets (xo and yo) based on the velocity of the belt, making sure to mark the sprite dirty for redraw:

function Belt:update()
    Belt.super.update(self)

    -- collision logic for affecting objects on belt removed for this example

    self.xo -= self.velocity.x * dt
    self.yo -= self.velocity.y * dt
    self:markDirty()
end

Finally, I sample this pattern image using the offsets when drawing the sprite. Because the pattern doesn't shift every frame, these offsets are incremented as floats. As such, they need to be floored and modded by 8 to decide which 8x8 section of our 16x16 pattern image to draw.

function Belt:draw()
    gfx.pushContext()
        -- draw a border
        gfx.setColor(gfx.kColorBlack)
        gfx.setLineWidth(2)
        gfx.drawRect(0, 0, self.width, self.height)

        -- animate the belt using our chosen pattern image and time-based offsets
        gfx.setPattern(self.beltImage,
            math.floor(self.xo) % 8,
            self.diagonal and 0 or math.floor(self.yo) % 8)
        gfx.fillRect(0, 0, self.width, self.height)
    gfx.popContext()
end

The one oddity in there is the "ternary" used to zero out the Y offset for diagonal belts. We only need to move a diagonal pattern in one axis to animate it, and in fact moving it in both axes causes an unwelcome flashing effect. There might be a cleaner way to deal with that, but it works.

I hope someone finds this helpful! Let me know if you have a better way to achieve this effect.

1 Like

To one-up the last version, I've since put together a little utility class to make this quick and easy. More details in this thread.

4 Likes

I just added this to my Tank Golf game to animate the pattern on the water and it couldn't have been easier!

It took me longer to decide on an easing function to use than it did to integrate your library.

So, many thanks!

1 Like

That's awesome! Glad it came in handy. :slight_smile: