Larger patterns and/or phase offsets for tiled images

I like to animate the phase offsets of patterns to create simple animated texture effects. However, the 8x8 size is quite limiting — I often wish I could get just a little more detail and/or a little more distance before the pattern repeats. Those pixels are tiny!

This is a multipart request, as I can see two independently useful ways to achieve what I'm after.

  1. I'd love support for 16x16 (HD? :sweat_smile: ) patterns. I suspect there are probably good technical reasons (e.g. performance) that it's constrained to 8x8, but this would address most of my current use cases.
  2. A way to draw an arbitrary image with x and y phase offsets. This could take a standalone form as in image:drawPhased(xPhase, yPhase), or be added to the provided image tiling capability as in image:drawTiled(x, y, width, height, [flip], [xPhase, yPhase])

This could be used to:

  1. Achieve animated tiled pattern effects of arbitrary size
  2. Render simple looping image elements or even whole backgrounds/scenes

Is there a good technique for achieving this effect already in the SDK? I suppose it would be possible by using image:draw(x, y, [flip, [sourceRect]]) by stitching together draws using sourceRects for each half (or each quadrant, if phasing in both axes), but that feels cumbersome to set up and potentially non-performant.

Thanks!

Well, this may not be the best approach, but it seems to get the job done:

function playdate.graphics.image:drawPhased(x, y, xPhase, yPhase)
    local w = self.width    -- shorthand width
    local h = self.height   -- shorthand height
    local _x = xPhase % w   -- constrained phase in x axis
    local _y = yPhase % h   -- constrained phase in y axis
    local w_ = w - _x       -- width of left half
    local _w = _x           -- width of right half
    local h_ = h - _y       -- height of top half
    local _h = _y           -- height of bottom half    

    self:draw(x,      y,      nil, _x,  _y, w_, h_) -- TL
    self:draw(x + w_, y,      nil,  0,  _y, _w, h_) -- TR
    self:draw(x,      y + h_, nil, _x,   0, w_, _h) -- BL
    self:draw(x + w_, y + h_, nil,  0,   0, _w, _h) -- BR
end

Result:
phased image

You could check if one of the phases is 0 in order to reduce the number of draw calls by half in those cases, but I'm not sure how much time that would save. Note that I tried adding a flip parameter and just passing that through to each draw call, but it does not work as I'd hoped. It does flip the image in the specified axis, but for some reason it cancels out the phase behavior in the opposite axis. I don't have enough brain power left this evening to determine why or how to fix it.

With this, one can then achieve a phased tile effect. Since this function calls the existing SDK image:drawTiled, it requires a unique name of its own:

function playdate.graphics.image:drawTiledWithPhase(x, y, w, h, flip, xPhase, yPhase)

    -- create a phased version of ourselves to tile
    local tile = playdate.graphics.image.new(self.width, self.height)
    playdate.graphics.pushContext(tile)
        self:drawPhased(0, 0, xPhase, yPhase)
    playdate.graphics.popContext()
 
    -- draw the phased tile
    tile:drawTiled(x, y, w, h, flip)
end

Tile:
dot-checker

Result:
Phased and tiled

Paired with the ability to stencil, I think this gives me a way to achieve the results I'm after. I'd still love larger patterns, which make it easier to use for drawing. I'd also love to see these added to the SDK, and any help getting flip working for drawPhased is welcome!

1 Like

Managed to get flip working properly for drawPhased:

function playdate.graphics.image:drawPhased(x, y, xPhase, yPhase, flip)
    local w = self.width    -- shorthand width
    local h = self.height   -- shorthand height
    local xp = xPhase % w   -- constrained phase in x axis
    local yp = yPhase % h   -- constrained phase in y axis

    local w_ = w - xp       -- width of left half
    local _w = xp           -- width of right half
    local h_ = h - yp       -- height of top half
    local _h = yp           -- height of bottom half

    local x_ = x            -- x coord of left half
    local _x = x + w_       -- x coord of right half
    local y_ = y            -- y coord of top half
    local _y = y + h_       -- y coord of bottom half

    local gfx = playdate.graphics
    if flip == gfx.kImageFlippedX or flip == gfx.kImageFlippedXY then
        x_, _x = _x, x_
        w_, _w = _w, w_
        xp = w - xp
    end
    if flip == gfx.kImageFlippedY or flip == gfx.kImageFlippedXY then
        y_, _y = _y, y_
        h_, _h = _h, h_
        yp = h - yp
    end
                                            -- (when unflipped)
    self:draw(x_, y_, flip, xp, yp, w_, h_) -- TL
    self:draw(_x, y_, flip,  0, yp, _w, h_) -- TR
    self:draw(x_, _y, flip, xp,  0, w_, _h) -- BL
    self:draw(_x, _y, flip,  0,  0, _w, _h) -- BR
end
1 Like