Easy animated patterns for Playdate.
EasyPattern is a utility for use with the Playdate SDK that provides a simple declarative syntax for creating animated patterns. Specify an 8x8 pattern sequence and any of a variety of easing parameters, and let EasyPattern take care of the rest. Under the hood it will automatically phase shift your pattern in the horizontal and/or vertical axes to create a seamless looping pattern texture that can be used with any PlayDate drawing calls.
Playdate is a registered trademark of Panic.
-
Download the EasyPattern.lua file.
-
Place the file in your project directory (e.g. in the
source
directory next tomain.lua
). -
Import it in your project.
import "EasyPattern"
Using toybox.py
-
If you haven't already, download and install
toybox.py
. -
Navigate to your project folder in a Terminal window.
cd "/path/to/myProject"
-
Add EasyPattern to your project
toybox add ebeneliason/easy-pattern toybox update
-
Then, if your code is in the
source
directory, import it as follows:import '../toyboxes/toyboxes.lua'
Define your pattern:
local checkerboard <const> = { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F }
local easyCheckerboard = EasyPattern {
pattern = checkerboard,
duration = 1.0,
ease = playdate.easingFunctions.inOutCubic,
-- <additional animation params here>
}
Set the pattern for drawing (e.g. in your sprite's draw
function):
playdate.graphics.setPattern(easyCheckerboard:apply())
-- draw using your pattern here
That's it! The pattern will automatically animate according to the parameters provided. The only
thing left to do is to make sure your sprite has a chance to draw in order to animate the pattern.
If it isn't naturally marked dirty (by e.g. moving each frame, etc.), you'll need to call
markDirty()
yourself each frame.
Depending on the speed of your animation, chances are the pattern won't actually update every
frame. To improve performance, you can check to see whether the phase values for the pattern have
changed in order to know when to mark your sprite dirty. You can do this from your sprite's
update
function:
if myEasyPattern:isDirty() then
self:markDirty()
end
EasyPattern takes a single argument — a table of named parameters that define both the pattern and
animation properties. (This is also why no parentheses are required when defining a new instance,
instead enabling use of {
and }
by themselves.)
Most parameters come in pairs to enable setting independent values for the X and Y axes. For
example, xDuration
and yDuration
. However, when initializing a new EasyPattern
, any
of the axis-specific values may be set for both axes at once by dropping the x
or y
prefix
from the parameter name, e.g. ..., scale = 2, reverses = true, ...
and so on. The usage example
above demonstrates this by setting the duration
and ease
for both axes at once.
EasyPatterns are designed to loop continuously. They do so with respect to an absolute clock that
starts the moment the program runs. They do not depend on timers. This approach means that two
instances of the same EasyPattern will run in sync with each other regardless of when they were
initialized or any other timing conditions. If you'd like two of the same EasyPatterns (or two
different patterns with the same duration) to animate out of phase with each other, adjust the
xOffset
and yOffset
for one of them.
A full list of supported parameters follows below. Technically speaking, none of these are required.
In practice, you'll want to set either pattern
or ditherType
, and a duration
for at least
one axis as shown in the example above.
The animation parameters may also be set directly on your EasyPattern instance at any time, e.g.
easyCheckerboard.xDuration = 0.5
The pattern to animate, specified as an array of 8 numbers describing the bitmap for each row, with
an optional additional 8 for a bitmap alpha channel, as would be supplied to
playdate.graphics.setPattern()
. See Defining Your Patterns for details
on how to construct valid arguments for this parameter.
Default: nil
A dither type as would be passed to playdate.graphics.setDitherPattern()
, e.g.
playdate.graphics.image.kDitherTypeVerticalLine
. This setting only applies when the pattern
parameter is omitted or nil
.
Default: nil
An alpha value for a dither pattern, which can either be the default Playdate dither effect, or one
specified by ditherType
. This setting only applies when the pattern
parameter is omitted or nil
.
Default 0.5
The color in which to draw the provided dither pattern. This setting only applies when the pattern
parameter is omitted or nil
.
Default: playdate.graphics.kColorBlack
The color to use as a background when rendering a dither pattern (or a pattern with an alpha channel, although in that case the background color could be baked into the pattern itself, without alpha). Patterns are rendered with transparency by default, but this can be used to make them opaque.
Default: playdate.graphics.kColorClear
An easing function that defines the animation pattern in the X axis. The function should follow the
signature of the playdate.easingFunctions
:
t
: elapsed time, in the range [0,duration
]b
: the beginning value (always 0)c
: the change in value (always 8 — the size of the pattern)d
: the duration (duration
)
Default: playdate.easingFunctions.linear
An easing function that defines the animation pattern in the Y axis. The function should follow the
signature of the playdate.easingFunctions
as described just above.
Default: playdate.easingFunctions.linear
A list containing any additional arguments to the X axis easing function, e.g. to parameterize amplitude, period, overshoot, etc.
Default: {}
A list containing any additional arguments to the Y axis easing function, e.g. to parameterize amplitude, period, overshoot, etc.
Default: {}
The duration of the animation in the X axis, in seconds. Omit this parameter or set it to 0 to prevent animation in this axis.
Default: 0
The duration of the animation in the Y axis, in seconds. Omit this parameter or set it to 0 to prevent animation in this axis.
Default: 0
An absolute time offset for the X axis animation (relative to Y), in seconds.
Default: 0
An absolute time offset for the Y axis animation (relative to X), in seconds.
Default: 0
A boolean indicating whether the X axis animation reverses at each end.
Default: false
A boolean indicating whether the Y axis animation reverses at each end.
Default: false
A boolean indicating whether the X axis animation is playing in reverse. This may be set manually,
and also updates automatically when xReverses
is true
.
Default: false
A boolean indicating whether the Y axis animation is playing in reverse. This may be set manually,
and also updates automatically when yReverses
is true
.
Default: false
A multiplier for the overall speed of the animation in the X axis, relative to the timings specified for its duration and offset.
Default: 1
A multiplier for the overall speed of the animation in the Y axis, relative to the timings specified for its duration and offset.
Default: 1
A multiplier describing the number of 8px repetitions the pattern moves by per duration cycle in the X axis. Non-integer values may result in discontinuity when looping.
Default: 1
A multiplier describing the number of 8px repetitions the pattern moves by per duration cycle in the Y axis. Non-integer values may result in discontinuity when looping.
Default: 1
This is where the magic happens. apply
takes no arguments and returns a 3-tuple matching the
signature of playdate.graphics.setPattern()
. This enables you to pass the result of a call to
apply
directly to the setPattern
function without intermediate storage in a local variable.
patternImage
: Aplaydate.graphics.image
containing the 8x8 pattern to be drawn.xPhase
: The calculated phase offset for the X axis given the current time and other animation properties.yPhase
: The calculated phase offset for the Y axis given the current time and other animation properties.
Indicates whether the pattern needs to be redrawn based on a change in the phase values since the
last time apply
was called. Note that isDirty
will only return true once when new phase
values get computed. If you need to check it multiple times per frame, such as when applying the
same pattern to multiple sprites, you'd need to cache it so all sprites can know whether a redraw
is required.
dirty
: A boolean indicating whether the pattern needs to be redrawn.
Used to introspect the current X and Y phase offsets for the pattern. If the values are stale, new values will be computed when calling this function; otherwise, the cached values will be returned instead.
xPhase
: A number representing the current phase offset for the X axis in the range 0..7.yPhase
: A number representing the current phase offset for the Y axis in the range 0..7.recomputed
: A boolean indicating whether the values were newly computed.
Sets a new pattern, retaining all animation properties.
pattern
: An array of 8 numbers describing the bitmap for each row, with an optional additional 8 for a bitmap alpha channel, as would be supplied toplaydate.graphics.setPattern()
.
Sets a new dither pattern, retaining all animation properties.
alpha
: A value in the range [0, 1] describing the opacity of the dither effect.ditherType
: (optional) A constant as would be passed toplaydate.graphics.setDitherPattern()
, e.g.playdate.graphics.image.kDitherTypeVerticalLine
.
Sets the color used for drawing the dither pattern.
color
: Aplaydate.graphics
color value.
These examples demonstrate the range of pattern animations possible with EasyPattern.
This example utilizes the built-in vertical line dither type to create a simple horizontally
scrolling conveyor belt effect. Because the dither effect naturally has transparency, a
bgColor
is specified so that the resulting belt pattern is fully opaque.
EasyPattern {
ditherType = playdate.graphics.image.kDitherTypeVerticalLine,
xDuration = 0.5,
bgColor = playdate.graphics.kColorWhite
}
This example creates a "marching ants" dotted outline effect, as is often used to indicate
rectangular selections. To achieve the effect, one would use this pattern in conjunction
with a call to drawRect(r)
. Modulate the length of the dashes with the alpha
parameter.
EasyPattern {
ditherType = playdate.graphics.image.kDitherTypeDiagonalLine,
xDuration = 0.25,
bgColor = playdate.graphics.kColorWhite
}
In this example, the pattern appears to fall downward one block at a time, bouncing to a settled state before the next row drops out.
EasyPattern {
pattern = checkerboard,
yDuration = 1.0,
yEase = playdate.easingFunctions.outBounce,
yReversed = true,
scale = 2
}
This example uses a sinusoidal ease in the vertical axis to create a simple wave motion, paired with a linear ease in the horizontal axis to illustrate directional flow. You can combine different easing functions and even different timing values for each axis to acheive more nuanced effects.
EasyPattern {
pattern = checkerboard,
xDuration = 0.5,
yDuration = 1.0,
yEase = playdate.easingFunctions.inOutSine,
yReverses = true,
}
This example makes use of built-in sine functions and an xOffset
to create a continuous
circular panning movement.
EasyPattern {
pattern = checkerboard,
duration = 0.5,
ease = playdate.easingFunctions.inOutSine,
xOffset = 0.25, -- half the duration
reverses = true,
scale = 2
}
This example introduces a custom easing function for more complex behavior. Technically, it's not an easing function at all, as it uses Perlin noise generation to return values in the desired range, causing the texture to appear to move about smoothly in a seemingly random way.
You could use this to create organic effects such as rustling leaves. You can create any type of custom function you like to design behaviors unique to your application.
EasyPattern {
pattern = checkerboard,
xDuration = 3,
yDuration = 2,
xEase = function(t, b, c, d) return b + playdate.graphics.perlin(t / d, 2, 3, 4, d, 0.75) * c end,
yEase = function(t, b, c, d) return b + playdate.graphics.perlin(t / d, 5, 6, 7, d, 0.75) * c end,
scale = 10
}
A variety of tools exist to help you find or create patterns you could use with EasyPattern. For instance, GFXP provides a library of patterns, a visual pattern editor, and a tool for viewing patterns on Playdate hardware.
You can specify your patterns in hex as shown in the examples above. Or, for a more direct visual representation in your code, you can use a binary encoding as shown below.
EasyPattern {
pattern = BitPattern {
'11110000',
'11100001',
'11000011',
'10000111',
'00001111',
'00011110',
'00111100',
'01111000',
},
-- animation properties…
}
BitPattern
is included when you import EasyPattern
so you can use it at your convenience.
You can also include an alpha channel for your pattern. BitPattern
automatically swizzles the
inputs, enabling you to place the pattern and its alpha channel side by side in a compact and legible
format, like so:
EasyPattern {
pattern = BitPattern {
-- PTTRN ALPHA
'10101010', '00010000',
'01010101', '00111000',
'10101010', '01111100',
'01010101', '11111110',
'10101010', '01111100',
'01010101', '00111000',
'10101010', '00010000',
'01010101', '00000000',
},
-- animation properties…
}
Make sure you've specified either the pattern
or the ditherType
parameters properly. More info
on defining your patterns is provided in the previous section.
- First, make sure you've properly specified an
xDuration
and/oryDuration
, without which your pattern will remain static. - Ensure that
draw
gets called as necessary to reflect the rendered pattern. If you're using a sprite, you can callself:markDirty()
from yourupdate
function. See the notes on performance to optimize drawing. If you're not using sprites, just be sure to call your draw method as needed each frame.
Playdate is a very capable device, but even relatively simple Lua programs can suffer from performance issues without adequate optimization. EasyPattern should work reliably in moderation for most games, and does have some built-in optimizations. Most notably, you can ensure that your sprite is only redrawn on frames when the pattern actually updates by checking whether it's dirty first:
-- only redraw the sprite when the pattern updates
if myEasyPattern:isDirty() then
self:markDirty()
end
When isDirty
is called, EasyPattern
will compute the phase offsets for the current time and
determine whether they have changed since the pattern was last applied. It also caches those
values so that they can be used when you do call apply
, avoiding the need to compute them twice
in a single frame. The caching also ensures that there's no performance hit for calling
apply
more than once in a given frame, so you can set the pattern multiple times in your draw
function as needed, or reuse the same pattern across several sprite instances with no penalty.
With all of that said, EasyPattern is certainly not the best approach to animated patterns for
performance given the need to calculate phase offsets each frame. If you need maximal performance
you should consider encoding each frame of the animated pattern in an imagetable
instead. If
you're using EasyPattern to draw sprites and need more performance, you can also use the
Roto utility to export the pattern or the final rendered
sprite(s) as matrix imagetable
images.
EasyPattern is distributed under the terms of the MIT License.