A list of helpful libraries and code

Not sure where I picked this function up at or if I wrote it, but I find myself using it all the time. It's very useful to scale a number across two different systems. For instance, if you have a number X that has values ranging from 100 to 500 (x_min and x_max), you can scale this down to a number between 1 and 10 (a and b). This might be useful to generate indexes for a lookup table or scale enemy speed by the player's score, or whatever else you can think of.

-- scale x between a and b, given x's min and max range
-- returns a number between a and b
function linearScaleBetween(_x,_a,_b,_xMin,_xMax)
    return (((_b-_a)*(_x-_xMin))/(_xMax-_xMin))+_a
end

Another simple method I get a lot of mileage out of is just a random table item generator:

function getRandomTableItem(_table)
    return _table[ math.random( #_table ) ]
end

I find myself using both of these in every project I make. Enjoy

1 Like

Hey @professir! Good one. In that vein, here are some functions I use that are complementary to the one you shared:

-- Get a version of val that does not spill outside of min and max.
-- Example: clamp(1.5, -1.1, 1.1) returns 1.1
function clamp(val, min, max)
  return val < min and min or val > max and max or val
end


-- Find number between two numbers (a, b) at t [0, 1]
-- Example: lerp(10, 30, 0.5) returns 20 (halfway between 10 and 30).
function lerp(a, b, t)
  return math.ceil(a * (1 - t) + b * t)
end

-- Find value [0, 1] between two numbers (a, b) at v
-- Example: invlerp(10, 30, 20) returns 0.5
function invlerp(a, b, v)
  return clamp((v - a) / (b - a), 0, 1)
end

-- Finds value [0, 1] of input between output.
-- Example: remap(20, 10, 30, 30, 50) returns 40 as 20 is halfway between 10 and 30 and remapped to new range 30 to 50 is 40. This function allows you to remap a value between two ranges.
function remap(input_val, input_min, input_max, output_min, output_max)
  return lerp(output_min, output_max, invlerp(input_min, input_max, input_val))
end
4 Likes

Here's something I put together after finding myself repeatedly reaching for animated patterns. I'll acknowledge up front that this isn't the best approach when it comes to performance (an imageable would be faster; also, I haven't done any optimization work here yet). That said, it's quick and fun to prototype with. (I'll also plug the Roto utility I made which could be used to export the animated output from this to a sprite sheet as needed.)

Usage

Initialize with a table containing the pattern and animation properties (see the full implementation below for a complete list), and then all it takes is one line to apply it when drawing…

-- initialize
local checkerboard = {0xF0F0, 0xF0F0, 0xF0F0, 0xF0F0, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F}
self.easyp = EasyPattern {
    pattern       = checkerboard,
    phaseDuration = 1.0,
    phaseFunction = playdate.easingFunctions.inOutCubic,
    -- <list any additional animation params here>
}

-- in draw function
playdate.graphics.setPattern(self.easyp:apply())

Examples

Here's an example of a simple horizontal conveyor belt effect:

self.easyp = EasyPattern {
    ditherType     = gfx.image.kDitherTypeVerticalLine,
    xPhaseDuration = 0.5
}

One for a downward stair bounce effect:

self.easyp = EasyPattern {
    pattern        = checkerboard,
    yPhaseDuration = 1.0,
    yPhaseFunction = playdate.easingFunctions.outBounce,
    yReversed      = true,
    scale          = 2
}

One that pans in a circular motion:

self.easyp = EasyPattern {
    pattern        = checkerboard,
    phaseDuration  = 0.5,
    phaseFunction  = playdate.easingFunctions.inOutSine,
    xPhaseOffset   = 0.25,
    reverses       = true,
    scale          = 2
}

And finally one that demonstrates a custom easing function. Here I'm generating a "random" Perlin noise movement, which could be used for e.g. simulating leaves rustling.

self.easyp = EasyPattern {
    pattern        = checkerboard,
    xPhaseDuration = 3.5,
    yPhaseDuration = 2.5,
    xPhaseFunction = function(t, b, c, d) return playdate.graphics.perlin(t / d, 1, 2, 3, 3, 0.5) end,
    yPhaseFunction = function(t, b, c, d) return playdate.graphics.perlin(t / d, 4, 5, 6, 3, 0.5) end,
    scale          = 50
}

Implementation

The code isn't doing anything especially complicated, but I did get a bit carried away with the parameterization so file length grew quickly. To make it easier to view the code and docs, I threw it up on GitHub:

Any thoughts on optimization are welcome!

5 Likes

Here's a minimal Settings class that handles defaults, fetch, and store on top of Datastore.

class("Settings").extends()

function Settings:init(default_settings)
	self.defaults = default_settings or {}
	self.settings = nil
	self:load()
end

function Settings:set(k, v)
	self.settings[k] = v
end

function Settings:get(k)
	return self.settings[k]
end

function Settings:save()
	playdate.datastore.write(self.settings, "settings", true)
end

function Settings:load()
	self.settings = table.deepcopy(self.defaults)
	local current_settings = playdate.datastore.read("settings") or {}
	for k, v in pairs(current_settings) do
		self.settings[k] = v
	end
end

An example of how I use it:

-- main.lua

-- I like to keep my settings keys global and descriptive.
SettingKeys = {
  helpEnabled = "help enabled",
  deaths = "death count"
}

-- Create a global settings object with default values for keys.
settings = Settings({
  [SettingKeys.helpEnabled] = true,
  [SettingKeys.deaths] = 0
})

-- example.lua
playdate.getSystemMenu():addCheckmarkMenuItem("help", settings:get(SettingKeys.helpEnabled), function(enabled)
  settings:set(SettingKeys.helpEnabled, enabled)
  settings:save()
end)

7 Likes

Apologies for the cross-post, but after sharing about a visual debugging tool that I put together I was reminded of this thread and realized it would be a handy place for others to find it, so I'll just drop the link:

1 Like

Thanks for that trick! It's a bit less of a general utility function this way, but you can take the wrapper up one level and use table calling syntax to make it a touch cleaner:

-- craft a well-formed pattern from a list of binary strings
function BitPattern (binaryRows)
    pattern = {}
    for i, binary in ipairs(binaryRows) do
        pattern[i] = tonumber(binary, 2)
    end
    return pattern
end

local BP = BitPattern

gfx.setPattern( BP {
    '11110000',
    '11100001',
    '11000011',
    '10000111',
    '00001111',
    '00011110',
    '00111100',
    '01111000',
})
2 Likes

Okay, one more extension of that same idea…

I don't use alpha channels for patterns often, but I didn't like that they took up so much vertical real estate in the file. This version automatically swizzles the inputs to enable placing the pattern and alpha channel definitions side by side, like so:

gfx.setPattern( BitPattern {
    -- PTTRN        ALPHA
    '10101010',  '00010000',
    '01010101',  '00111000',
    '10101010',  '01111100',
    '01010101',  '11111110',
    '10101010',  '01111100',
    '01010101',  '00111000',
    '10101010',  '00010000',
    '01010101',  '00000000',
})

This keeps things more compact, and I also find it easier to make sense of since the pattern and alpha channel rows can be compared side-by-side. Here's a function which supports patterns with or without alpha. It's highly specialized to 8x8 patterns, of course, but pretty convenient:

function BitPattern(binaryRows)
    local hasAlpha = #binaryRows == 16
    local pattern = {}
    for i, binaryRow in ipairs(binaryRows) do
        if hasAlpha then
            -- swizzle the rows to produce independent channels from interleaved inputs
            pattern[i//2 + (i % 2 == 0 and 8 or 1)] = tonumber(binaryRow, 2)
        else
            -- no alpha channel, nothing to swizzle
            pattern[i] = tonumber(binaryRow, 2)
        end        
    end
    return pattern
end
3 Likes

I have had an issue where table.push() did not work and I changed it to table.insert() but even then the images do not show? What are the image sizes you used? Thanks.

Just a couple of helper functions I wrote.

I'm finding with.graphicContext to be pretty handy, and might end up doing something specifically for white text.

with = {
   graphicContext = function(fn, ...)
      playdate.graphics.pushContext(...)
      fn()
      playdate.graphics.popContext()
   end,

   inputHandlers = function(fn)
      playdate.inputHandlers.push()
      fn()
      playdate.inputHandlers.pop()
   end,

   image = function(w,h,fn)
      local out = playdate.graphics.image.new(w, h)
      with.graphicContext(fn, out)
      return out
   end,
}

5 Likes

Seems like this doesn't work anymore due to polygon:setClosed not being in the SDK anymore.

I'll try to take a look this week. Should be a simple enough fix.

1 Like

Hi, I try to use this fluid class but I think is not working in the latest sdk. What must I do to use this?
Thanks so much

This needs an updated patch.
Its because it was developed during the very early days of Playdate SDK development. So many things are changed or are different than what was publicly available.
I think the best option is to completely rewrite it from scratch.

Ah, ok, thanks for the answer, I thought I was programming something bad. All clear now

fluid.zip (9.0 KB)
it was a one line fix, this one should work for you.

yep. it is polygon:close now.

Sorry I should update the original post, I have this code on GitHub now with support for latest SDK. And the ability to submit pull requests and such that come with GitHub so we can make the code better together. :slight_smile:

4 Likes

sent you a PR as well

1 Like

I wrote a ProTracker module (=Amiga music) player library in Lua. It's not free, but perhaps someone will find it useful enough to buy me a pizza. :pizza:

2 Likes

I just came up with this snippet after pondering an SDK feature request I made myself. Use it to draw images with phase offsets, such as continuously scrolling backgrounds, etc.:

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

For good measure, I added an option to tile a phased image as well, which provides a way to animate patterns larger than the standard 8x8. You could use this with a stencil to use large animated patterns for drawn shapes.

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


Update: Added support for flip to 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
8 Likes