Easy animated patterns for Playdate.
EasyPattern provides a simple way to define animated patterns for use with the Playdate SDK. Specify an 8x8 pattern—in one of several formats—and some easing parameters, and EasyPattern does the rest. It will automatically update your pattern each frame, creating a seamlessly looping pattern texture that you can use with any PlayDate drawing calls.
Playdate is a registered trademark of Panic.
- Installation
- Basic Usage
- Pattern Gallery
- Understanding EasyPattern
- Parameter Reference
- Function Reference
- Examples
- Demo Swatch
- Defining Patterns
- Troubleshooting & Performance
-
Download the EasyPattern.lua file.
-
Place the file in your project directory (e.g. in the
sourcedirectory next tomain.lua). -
Import it in your project.
import "EasyPattern"
-
If you haven't already, download and install
toybox.py. -
Add EasyPattern to your project directory:
toybox add ebeneliason/easy-pattern toybox update
-
Then, if your code is in the
sourcedirectory, import it as follows:import '../toyboxes/toyboxes.lua'
Create an EasyPattern using a simple declarative syntax:
local easyCheckerboard = EasyPattern {
pattern = { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F }, -- checkerboard
duration = 1.0,
ease = playdate.easingFunctions.inOutCubic,
-- more params here…
}See the docs for init() and the list of supported parameters.
Set the pattern for drawing with apply(), for example in your sprite's draw() function):
playdate.graphics.setPattern(easyCheckerboard:apply())
-- draw with your pattern here, using any of the SDKs drawing APIsThat's it! The pattern will automatically animate according to the parameters provided. If using sprites, make sure your sprite has a chance to draw in order to animate the pattern (see below).
Depending on the speed of your animation, chances are the pattern won't update every frame. You can check to see
whether the pattern has changed with isDirty() in order to know when to redraw.
If using sprites, you can mark them dirty in your sprite's update() function:
if easyCheckerboard:isDirty() then
self:markDirty()
endClick on a pattern to jump to an example with code, or use the demo swatch to try them for yourself. If you come up with your own patterns, come share them in the Playdate Squad Discord channel!
EasyPatterns can be thought of in two parts:
- Pattern: A collection of properties that define its overall appearance
- Animation: A collection of properties that define its overall behavior
This section provides context for each of these to help you understand how EasyPattern works, which will
help you reason about how to construct your patterns. The pattern properties converge to define an 8x8
pixel pattern image, and the animation properties converge to define the phase offsets for the pattern in each axis
at a given point in time. These converged values are returned by each call to apply(), enabling
you to pass them directly to playdate.graphics.setPattern() and draw using the animated pattern.
The types of the core pattern and animation properties match those used elsewhere in the Playdate SDK to maximize compatibility.
- Patterns are defined in one of several formats, including an array of 8 numbers describing
the bitmap for each row, with an optional additional 8 for a bitmap alpha channel (as would be passed to
playdate.graphics.setPattern()). - Easing functions are defined in the
playdate.easingFunctionsformat. You can use these functions directly, reference another library, or define custom functions of your own.
EasyPatterns support several parameters representing distinct layers which get composited to create the final pattern. This diagram describes their order.
↑ TOP
block-beta
columns 8
M["alpha (dither mask)"]:8
A["pattern"]:8
B["bgPattern"]:2
block:bgPattern:6
columns 1
W["alpha (dither mask)"]
X["pattern"]
Y["bgPattern…"]
Z["bgColor"]
end
C["bgColor"]:8
classDef mask fill:#FFFFFF05, stroke-dasharray: 5 5
class M mask
class W mask
↓ BOTTOM
A few notes about pattern composition:
-
Because the
bgPatternproperty may be set to anotherEasyPatterninstance, it's possible to create compositions which combine two or more patterns together as one. -
Patterns may apply an opacity effect via the
alphaandditherTypeproperties, which applies to all layers of the pattern it's defined on and any nested more deeply beneath it. -
Only the pattern layer is affected by phase adjustments. The background pattern is only subject to its own phase, while the background color and alpha mask remain fixed even as the pattern itself shifts in phase.
Note
The fully-composited pattern image is accessible via the compositePatternImage property. Note that this
represents the raw pattern, before any phase shifts have been applied. However, you should rarely need to access
this—it's easiest to draw with your pattern by calling apply().
EasyPatterns are designed to loop continuously. They do so with respect to an absolute clock that
starts the moment the program runs (specifically playdate.getCurrentTimeMilliseconds()). They do not depend on
timers. Instead, the phase shifts for each axis at the current point in time are computed in closed-form based
on the system clock and their animation properties.
Note
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/or yOffset for one of them.
To understand how these properties affect the animation, let's look at how the phase is computed at a given point in time:
-- assuming time t, xEase ~= nil, and xDuration > 0...
local tx = (t * self.xSpeed + self.xOffset) % self.xDuration
local xPhase = self.xEase(tx, 0, PTTRN_SIZE, self.xDuration, table.unpack(self.xEaseArgs)) * self.xScale % PTTRN_SIZE // 1First, note the time scaling. tx is computed based on the current time t by scaling it by xSpeed (thus slowing
or speeding up time), adding the xOffset (thus adjusting the start time), and then modding by xDuration (so the
value passed to the easing function is always in the range [0, xDuration]).
The adjusted time value is then passed to the specified easing function, which interpolates between 0 and 8
(PTTRN_SIZE) over the specified xDuration. That result is then multiplied by xScale to amplify the phase
shift (for example, setting xScale to 2 will cause the pattern to move 16px per loop). Lastly, the result is
modded by 8 and integer-divided by 1 to truncate the final phase to an integer value in the range [0,7].
You can also define other properties that affect the final animation in addition to those that define core timing:
- Reverses: Set
xReversesoryReversesto cause the animation to reverse directions at each end. ThexReversed/yReversedboolean properties will flip with each reversal. - Reversed: Set
xReversedoryReversedto cause the animation to run in the opposite direction. This may be used with or without "reverses".
Transformation properties apply to both the fully composited pattern and its easing animations. These properties enable holistic changes to your pattern without needing to calculate adjustments to individual properties.
- Reflection: Set the
xReflectedandyReflectedproperties to mirror the fully-composited pattern in the horizontal and vertical axes, respectively. - Rotation: Set the
rotatedproperty to rotate the fully-composited pattern by 90º, producing an orthogonal result. - Translation/Phase: Set the
xShiftandyShiftproperties to additively adjust the phase offsets of the pattern in each axis. This can be used to make patterns respond dynamically to inputs or game states, as described insetPhaseShifts. - Inversion: Set the
invertedproperty to cause all white pixels to appear black, and vice-versa.
A full list of supported parameters follows below. Pass a single table containing one or more of these
parameters to init() to define your pattern.
Parameters are grouped into the following categories:
- Pattern Parameters: Define the overall appearance of your pattern.
- Animation Parameters: Define the easing behaviors of your pattern.
- Transformation Parameters: Apply simple transformations to your pattern, such as translation, reflection, and rotation.
- Callback Parameters: Set functions to be called when the pattern loops or new phases get calculated.
The animation and callback parameters may also be set directly on your EasyPattern instance at any time after
initialization, e.g.
easyCheckerboard.xDuration = 0.5An 8x8 pixel pattern specified in one of these formats:
-
Bit Pattern: An array of 8 numbers describing the bitmap for each row, with an optional 8 additional for a bitmap alpha channel (as would be passed to
playdate.graphics.setPattern()). See Defining Your Patterns for additional detail on how to construct patterns in this format.Example:
pattern = { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F }(checkerboard) -
Dither Pattern: A table containing:
- A
ditherType(as would be passed toplaydate.graphics.setDitherPattern(). - An optional
alphavalue in the range [0,1]. Default:0.5. - An optional
colorvalue in which to render the dither. Default:playdate.graphics.kColorBlack.
Example:
pattern = { ditherType = playdate.graphics.image.kDitherTypeDiagonalLine, alpha = 0.75, }
As a convenience, if you intend to render the pattern in black at 50% alpha you can assign a bare dither type constant to the pattern parameter, e.g.,
pattern = playdate.graphics.image.kDitherTypeHorizontalLine. - A
-
Image: An 8x8 pixel
playdate.graphics.image.Example:
pattern = playdate.graphics.image.new("images/myPattern") -
Image Table: An 8x8 pixel
playdate.graphics.imagetable(for animated patterns). See also:tickDuration.Example:
pattern = playdate.graphics.imagetable.new("images/myPattern") -- filename: "myPattern-table-8-8.png" or "myPattern.gif"
You can call the overloaded setPattern() or setBackgroundPattern() functions with any of
these pattern types to change the pattern after your EasyPattern has been instantiated.
Default: { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F } (checkerboard)
Tip
You can quickly test out new easing behaviors with the default checkerboard pattern before creating your own.
A pattern to render behind the this one. This supports any pattern type supported by the pattern
parameter above, or another EasyPattern instance. Overlaying EasyPatterns can create interference patterns and
more complex animation behaviors. See Composite Patterns for an example.
Default: nil
The color to use as a background. This is especially useful when specifying a dither pattern, but may be used with any transparent pattern.
Default: playdate.graphics.kColorClear
An alpha value indicating the opacity at which to render the pattern. Adjusting this value results in an effect
similar to that provided by playdate.graphics.image:drawFaded(). The dither pattern used can be changed via the
ditherType property. The alpha dither remains fixed with respect to the screen even as the pattern itself shifts
in phase, causing the pattern to appear to move "beneath" the alpha mask.
See Translucent Patterns for an example.
Default 1.0
A dither type used to render the pattern with reduced opacity when alpha is less than 1. This property accepts any
values supported by playdate.graphics.setDitherPattern().
Default: playdate.graphics.image.kDitherTypeBayer8x8
A boolean indicating whether the pattern is inverted, with any white pixels appearing black, and any black pixels appearing white. Inverting the pattern does not affect the alpha channel.
Default: false
The number of seconds per "tick", used to determine how long each image of the sequence is shown when either the
pattern and/or background pattern is an imagetable.
See Animated Patterns for an example.
Default: The target FPS, i.e. 1 / playdate.display.getRefreshRate()
An easing function that defines the animation 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
Default: playdate.easingFunctions.linear
Note
Although a linear ease is set by default, it has no effect unless you provide a duration for one or both axes.
An easing function that defines the animation 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, in seconds.
Default: 0
An absolute time offset for the Y axis animation, 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.
Default: 1
A multiplier for the overall speed of the animation in the Y axis.
Default: 1
A multiplier describing the number of 8px repetitions the pattern moves by per cycle in the X axis.
Default: 1
Important
Non-integer values will result in discontinuity when looping.
A multiplier describing the number of 8px repetitions the pattern moves by per cycle in the Y axis.
Default: 1
Important
Non-integer values will result in discontinuity when looping.
A boolean indicating whether the entire pattern should be reflected across the vertical (Y) axis. See Reflected Patterns for an example.
Default: false
A boolean indicating whether the entire pattern should be reflected across the horizontal (X) axis. See Reflected Patterns for an example.
Default: false
A boolean indicating whether the entire pattern should be rotated 90º, producing an orthogonal result. Rotation is applied following any reflections.
Default: false
The number of pixels to shift the pattern's phase by in the X axis. This is additive to any computed phase based on other animation properties, and is applied following any reflections or rotations.
Default: 0
The number of pixels to shift the pattern's phase by in the Y axis. This is additive to any computed phase based on other animation properties, and is applied following any reflections or rotations.
Default: 0
These callbacks trigger when the pattern loops—in the X axis, the Y axis, or overall. You can use these callbacks to modify the pattern itself, or to trigger other effects in sync with its movement. See Self-Mutating Patterns for an example.
Important
Because EasyPattern does not use timers, these callbacks trigger lazily when the pattern crosses a loop boundary
while computing new phase offsets (such as when checking isDirty(), or when calling apply()). If you check for
dirty and/or draw using your pattern each frame, you can ignore this fact. Otherwise, be aware that:
- The time between loop callbacks may not be exact, especially if the frame rate is lower.
- The callbacks will not be called at all if the pattern is not being used.
A function to be called when the pattern loops, taking into account the effective duration of the animation
in each axis including speed and reversal, as well as any animated background pattern. The EasyPattern and
total loop count are passed as parameters to the function.
myEasyPattern.loopCallback = function(p, n)
print("Looped " .. n .. " times!", p)
endDefault: nil
A function to be called when the pattern loops in the X axis, taking into account speed and reversal, as
well as any background pattern. The EasyPattern and X loop count are passed as parameters to the function.
Default: nil
A function to be called when the pattern loops in the Y axis, taking into account speed and reversal, as
well as any background pattern. The EasyPattern and Y loop count are passed as parameters to the function.
Default: nil
A function to be called immediately before phase computation. You can use this to make any dynamic
adjustments to the pattern based on the current time, game state, or external inputs like the crank.
The EasyPattern and the current time are passed as parameters to the function.
For example, the following function updates the Y axis phase shift based on crank input:
myPattern.update = function(p) p.yShift = playdate.getCrankPosition()//15 endSee Dynamic Patterns for a complete example with visual.
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, enabling use of { and } alone.)
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. duration = 1, scale = 2, reverses = true, ... and so on. For example:
local myPattern = EasyPattern {
pattern = { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F }, -- checkerboard
duration = 1,
yEase = playdate.easingFunctions.inOutSine,
yReverses = true,
}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:
gfx.setPattern(myPattern:apply())
-- draw using your pattern…Returns:
patternImage: Aplaydate.graphics.imagecontaining the 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 or pattern image since
the last time apply() was called. In practice, this means you can check to see if the pattern is dirty
in update() and call markDirty() on your sprite to ensure draw() gets called that frame. This
will work no matter how many sprites use the same pattern for drawing.
-- e.g. in `sprite:update()`
if myPattern:isDirty() then
self:markDirty()
endNote
If you aren't using sprites and intend to apply the same pattern multiple times per frame, be sure to cache the
result of calling isDirty() each frame before you apply the pattern, or else subsequent checks will return false.
Returns:
dirty: A boolean indicating whether the pattern needs to be redrawn.
Returns the current X and Y phase offsets for the pattern. If the values are stale, new values are
computed when this function is called; otherwise, cached values are returned. You generally won't need
to call this function directly, as it gets called every time you call isDirty() or apply().
Returns:
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.
Returns a new copy of the EasyPattern with identical properties. An optional list of parameters may be
provided to override properties in the returned copy. For example, this can be used to quickly create inverted,
reflected, or rotated versions of the original pattern:
local myPatternReflection = myPattern:copy {
inverted = true, -- flip colors
yReflected = true, -- mirror vertically
}Params:
overrides: A table containing a list of properties to override in the newly copied instance, in the same format as provided toinit().
Returns:
copy: TheEasyPatterncopy.
The pattern and background pattern may be set with the functions below. The provided overrides to setPattern(...)
and setBackgroundPattern(...) taken together allow setting new patterns using any of the formats supported
by the pattern param when passed to init().
Tip
Setting a background pattern is substantially more performant than drawing one pattern atop another, as only the 8x8 pattern gets composited (and only in frames when it changes). All other drawing is only done once.
Given that you can set another EasyPattern as a background, you can also create chains to compose 3 or more
patterns and achieve more complex effects.
Sets a new pattern with a bitmap provided as a sequence of bytes.
Params:
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().
Generates a new pattern with the provided ditherType, as well as the optional alpha and color values.
The pattern is rendered with transparency, by default. To obtain an opaque result, set a complementary bgColor
on the EasyPattern.
Params:
ditherType: The dither to render as a pattern, which may be any supported byplaydate.graphics.setDitherPattern().[alpha]: The opacity to render the dither pattern with. Default:0.5.[color]: An optionalplaydate.graphicscolor to render the pattern in. Default:playdate.graphics.kColorBlack.
Sets the pattern using the provided image.
Params:
image: Aplaydate.graphics.image, which should be 8x8 pixels in size.
Sets the pattern using the provided image table. Once set, the pattern will automatically update to show a looping sequence of images from the image table, advancing once per tick.
Params:
imageTable: Aplaydate.graphics.imagetable, which should be 8x8 pixels in size and may have as many images as desired.[tickDuration]: An optional value specifying the number of seconds between ticks at which the pattern advances to the next image in the sequence. This is a convenience which sets thetickDurationproperty when setting a pattern, though it may also be set directly.
Note
The number of images in the provided sequence has no effect on the X, Y, or total calculated loop duration, which reports only with respect to the easing animation loops.
Sets a new background pattern with a bitmap provided as a sequence of bytes.
Params:
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 background pattern generated using the provided ditherType and alpha values, as well
as an optional color in which to render the pattern. The pattern is rendered with transparency, by
default. To obtain an opaque result, set a complementary bgColor on the EasyPattern.
Params:
ditherType: The dither pattern to render as a pattern, which may be any value supported byplaydate.graphics.setDitherPattern().[alpha]: The opacity to render the dither pattern with. Default:0.5.[color]: An optionalplaydate.graphicscolor to render the pattern in. Default:playdate.graphics.kColorBlack.
Sets the background pattern using the provided image.
Params:
image: Aplaydate.graphics.image, which should be 8x8 pixels in size.
Sets the background pattern using the provided image table. Once set, the pattern will automatically update to show a looping sequence of images from the image table, advancing once per tick.
Params:
imageTable: Aplaydate.graphics.imagetable, which should be 8x8 pixels in size and may have as many images as desired.[tickDuration]: An optional value specifying the number of seconds between ticks at which the pattern advances to the next image in the sequence. This is a convenience which sets thetickDurationproperty when setting a pattern, though it may also be set directly.
Note
The number of images in the provided sequence has no effect on the X, Y, or total calculated loop duration, which reports only with respect to the easing animation loops.
Sets the background pattern to another EasyPattern instance, enabling composition of multiple animated layers.
The background EasyPattern will animate independently of its parent (its X and Y phases will not be impacted by
those of the overlaid pattern).
Note
The X, Y, and total loop durations of the parent EasyPattern will adjust to account for those of its background,
such that the reported values indicate a return of both patterns to their initial state. This may result in long
loop durations when their specified durations are co-prime, or essentially infinite loop durations when one is a
non-divisible fraction of the other.
Params:
easyPattern: AnEasyPatterninstance to use as a background.
Sets the background color shown behind any transparent areas in the pattern and bgPattern.
Params:
color: Aplaydate.graphicscolor constant.
Sets the opacity of the fully-composited pattern.
Params:
alpha: The desired opacity, in the range [0, 1].[ditherType]: The dither type used as an alpha mask, which may be any value supported byplaydate.graphics.setDitherPattern().
Inverts the resulting pattern, causing any white pixels to appear black and any black pixels to appear white. The alpha channel is not affected.
Params:
flag: Abooleanindicating whether the pattern is inverted.
Sets the X and Y phase shift values. If yShift is omitted, both X and Y phases are set to the same value.
These phase shifts are additive to (rather than override) any shifts in phase resulting from the animation
parameters applied to your pattern.
This can be used to enable dynamic pattern behaviors driven by external game logic. Here are a few examples:
- A conveyor belt pattern which animates according to the crank speed
- A landscape pattern (e.g. platforms, trees) that's shifted a bit for each instance to help them appear as distinct entities even though they share the same pattern
- A parallax effect, which shifts the animated pattern laterally as the player moves left and right
The image below illustrates one such application. The phases of the EasyPattern applied to the ball
are shifted dynamically based on the ball's velocity. This creates the illusion that the ball is rolling
and spinning as it travels down the lane.
Params:
xShift: The phase shift in the X axisyShift: The phase shift in the Y axis
Note that these values may also be set directly on an EasyPattern instance. However, calling this function
ensures that the resulting phase values are correct immediately, rather than lazily computed the next time
the pattern is applied.
A convenience function that sets the phase shifts by offsetting them from their current values by the specified amount.
If yShift is omitted, both X and Y phases are shifted the same amount.
Params:
xShift: The amount to to shift the phase by in the X axisyShift: The amount to to shift the phase by in the Y axis
Sets the xReflected and yReflected properties indicating in which axes the pattern should be inverted.
If the second argument is omitted, both axes are set to the same value.
Params:
horizontal: Abooleanindicating whether the pattern is reflected horizontally across the Y axis.vertical: Abooleanindicating whether the pattern is reflected vertically across the X axis.
Sets the rotated property, indicating whether the pattern should be rotated 90º to produce an
orthogonal result.
Params:
flag: A boolean indicating whether the pattern is rotated.
Returns the total effective loop duration of the pattern in seconds, taking into account the duration of the animation in each axis including speed and reversal, as well as any background pattern.
Returns the total effective loop duration of the pattern in the X axis in seconds, taking into account its speed and reversal as well as any background pattern.
Returns the total effective loop duration of the pattern in the Y axis in seconds, taking into account its speed and reversal as well as any background pattern.
Note
These functions ignore the length of any sequences set on the pattern or background pattern using an imagetable.
They only reflect the total duration of easing animations in each axis or overall.
These examples demonstrate the range of pattern animations possible with EasyPattern. Each is shown with a standard checkerboard pattern to compare the easing effect, and with a custom pattern intended to illustrate a potential application.
You can try these examples yourself using the EasyPatternDemoSwatch. See below for instructions, as well as docs for BitPattern which enables the ASCII pattern representations seen in many of these examples.
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. Achieve belt effects
moving in different directions by either:
- Specifying the horizontal dither type and
reversedas needed - Using a combination of
reflectedandrotatedto reorient the pattern
Demo Swatch ID: conveyor
EasyPattern {
pattern = playdate.graphics.image.kDitherTypeVerticalLine,
duration = 0.5,
bgColor = playdate.graphics.kColorWhite,
}Note
Because the provided dither pattern is rendered in black at 50% alpha, we can assign the dither constant directly to the pattern parameter.
This example uses a transparent horizontal line dither to create a simple scanline effect. This could be used atop an image or rendered scene to simulate an old display.
Demo Swatch ID: scanline
EasyPattern {
pattern = {
ditherType = playdate.graphics.image.kDitherTypeHorizontalLine,
color = gfx.kColorWhite,
alpha = 0.8,
},
duration = 0.5,
yReversed = true,
}Note
Unlike the previous example, a table is provided for the pattern parameter in order to set the color and
alpha values used to generate the pattern with the given the dither.
Adding a custom pattern to a default linear ease in the vertical axis produces imagery that evokes landscapes such as a waterfall, sand dunes, lava, or gooey ooze.
Demo Swatch ID: ooze
ooze = EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
' . . X X X X X . ',
' X . . X X X . . ',
' X X . . . . . X ',
' X X X X X X X X ',
' X X X X X X X X ',
},
yDuration = 1,
yReversed = true,
},This example creates a "marching ants" dotted outline effect, as is often used to indicate
rectangular selections. To achieve the effect, use this pattern in conjunction with a call
to drawRect().
Demo Swatch ID: ants
EasyPattern {
pattern = 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. The scale parameter is used to exaggerate
the effect, causing it to fall by multiple rows per cycle.
Demo Swatch ID: bounce
EasyPattern {
pattern = BitPattern {
' . . . . . . . . ',
' X X X . X X X X ',
' X X X . X X X X ',
' X X X . X X X X ',
' . . . . . . . . ',
' X X X X X X X . ',
' X X X X X X X . ',
' X X X X X X X . ',
},
yDuration = 1,
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 achieve more nuanced effects.
Demo Swatch ID: waves
EasyPattern {
pattern = BitPattern {
' . . . . . . . . ',
' . X . . . . . X ',
' . . . X . X . . ',
' . . . . . . . . ',
' . . . . . . . . ',
' X X X X . . . . ',
' . . . . X X X X ',
' . . . . . . . . ',
},
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.
Demo Swatch ID: circle
EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X . . . X X X ',
' X . X X X . X X ',
' X . X X X . X X ',
' X . X X X . X X ',
' X X . . . X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
},
duration = 1,
ease = playdate.easingFunctions.inOutSine,
xOffset = 0.5, -- half the duration
reverses = true,
scale = 3,
}Changing just a few parameters from the above example creates a gentle swaying motion.
Demo Swatch ID: sway
EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X X X X X X X ',
' X X X X . X X X ',
' . X X X X X X X ',
' . . X X X X X . ',
' . X . X X X . X ',
' X . X . . . X . ',
' X X . X . X . X ',
},
xDuration = 2,
yDuration = 1, -- half the x duration
ease = playdate.easingFunctions.inOutSine,
reverses = true,
yReversed = true,
xScale = 3,
}This example introduces a custom easing function for more complex behavior. Technically, it's not an easing function at all. It ignores the easing parameters in favor of returning a random offset value. This yields a jittery vibration effect evocative of high energy or volatility.
You can create any type of custom function you like to design behaviors unique to your application.
Demo Swatch ID: vibrate
EasyPattern {
pattern = BitPattern {
' . . . . . . . . ',
' . . . . . . . . ',
' . . . X . . . . ',
' . . . X X . . . ',
' . X X X X X . . ',
' . . X X . . . . ',
' . . . X . . . . ',
' . . . . . . . . ',
},
duration = 1, -- must be non-zero to trigger easing function, but value doesn't matter
scale = 2, -- adjust to change the amplitude of vibration
ease = function() return math.random(0,8)/8 end, -- note that all args are ignored
}[!NOTE] The same result can also be achieved using a custom
update()function. Check out EasyPatternDemoSwatch.lua to see how.
This example extends the concept introduced above, using Perlin noise to generate values which cause the texture to animate smoothly in a seemingly random way. You could use this to create organic effects such as rustling leaves.
Demo Swatch ID: perlin
EasyPattern {
pattern = BitPattern {
' . . . . . . . . ',
' . X . . . X . . ',
' . . . X . . . . ',
' . . . . . . . X ',
' . . . . . X . . ',
' . X . . . . . . ',
' . . . . . . . . ',
' . . . X . . . X ',
},
xDuration = 3,
yDuration = 2, -- non-equal durations extend the total loop time, increasing apparent randomness
xEase = function(t, b, c, d) return b + playdate.graphics.perlin(t / d, 2, 6, 8, d, 0.75) * c end,
yEase = function(t, b, c, d) return b + playdate.graphics.perlin(t / d, 5, 9, 9, d, 0.75) * c end,
scale = 10, -- values are in the range [0,1], so we need to magnify to see the effect
}Here's one more example showcasing a custom easing function. This foregoes the continuous motion of common
easing functions for a stepwise shift between the start and end values. Adjust the constant (4 in this example)
in the easing function to change the number of steps per loop.
Demo Swatch ID: dotmatrix
EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
' X X X . . X X X ',
' X X X . . X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
},
yDuration = 1,
-- the math below uses integer division to achieve the same result as math.floor(t*4)/4
yEase = function(t, b, c, d) return playdate.easingFunctions.linear(t*4//1/4, b, c, d) end,
}This example takes advantage of the update callback to create a blink effect, causing the pattern to invert
periodically. It uses the time value passed to the callback to know which way to set the inverted flag. You
can adjust the blink speed by changing the denominator.
Demo Swatch ID: blink
EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X X X X X . X ',
' X X X X X . . X ',
' X X X X . . . X ',
' X X X . . . . X ',
' X X . . . . . X ',
' X . . . . . . X ',
' X X X X X X X X ',
},
-- the divisor is the blink speed in milliseconds; decrease it to strobe faster!
update = function(p, t) p:setInverted(t*1000//1000 % 2 == 0) end
}This example introduces an alpha channel with a custom pattern. The use of adjacent white and black opaque pixels in the pattern enables it to read against either black or white background elements.
Demo Swatch ID: steam
EasyPattern {
pattern = BitPattern {
-- pattern -------- -- alpha ----------
' . . X . . X . . ', ' . . X . X X . . ',
' . X . . . . . . ', ' . X X . . . . . ',
' . X . . . . . . ', ' . X X . . . . . ',
' . . X . . . . . ', ' . . X X . . . . ',
' . . . . . . . . ', ' . . . . . . . . ',
' . . . . . X . . ', ' . . . . . X . . ',
' . . . . . . X . ', ' . . . . . X X . ',
' . . . . . . X . ', ' . . . . . X X . ',
},
duration = 1,
ease = playdate.easingFunctions.inOutSine,
yOffset = 0.5, -- half the duration
xReverses = true,
}You can easily create a reflection of any pattern you've already created by setting reflected to true
in your pattern declaration, or on the resulting pattern once instantiated. You can reflect horizontally,
vertically, or both. This saves the hassle of having to adjust individual animation parameters to
achieve the same effect. (A similar convenience is provided by the rotated flag, which rotates the
pattern orthogonally.)
Demo Swatch ID: reflected
EasyPattern {
pattern = {
ditherType = playdate.graphics.image.kDitherTypeDiagonalLine,
alpha = 0.2,
},
xDuration = 1,
xReflected = true,
bgColor = gfx.kColorWhite,
}Tip
You can easily create a reflected version of an existing pattern with copy() by passing overrides
to define the reflection.
local myPatternReflection = myPattern:copy {
inverted = true, -- flip colors
yReflected = true, -- mirror vertically
}Tip
To render both reflected and unreflected versions of a dynamic pattern (even one with randomness), retain a single
pattern instance and flip the xReflected or yReflected property before drawing the reflection:
-- draw the reflection
myPattern:setReflected(false, true) -- reflect vertically
gfx.setPattern(myPattern:apply()) -- apply the reflected version
gfx.fillRect(...)
-- draw the unreflected pattern
myPattern:setReflected(false) -- un-reflect to restore default state
gfx.setPattern(myPattern:apply()) -- apply the unreflected version
gfx.fillRect(...)You can layer patterns to create more complex effects. You can overlay an animated pattern on a static background or, as shown here, overlay two patterns with independent easing effects. The pattern shown below is a transparent variation of "ooze". It can be drawn atop an image to add a subtle effect, or used with "ooze" as a background pattern to create a richer, more textured animation.
Consider how you might use this with the provided "waves" example, or any patterns you create yourself.
Demo Swatch Name: waterfall
EasyPattern {
pattern = BitPattern {
-- pattern -------- -- alpha ----------
' . . . . . . . . ', ' . . . . . . . . ',
' . . . . . . . . ', ' . . . . . . . . ',
' . . . . . . . . ', ' . . . . . . . . ',
' . . . . . . . . ', ' . X . . . . . . ',
' . . . . . . . . ', ' . . X . . . . X ',
' . . . . . . . . ', ' . . . . X . X . ',
' . . . . . . . . ', ' . . . . . . . . ',
' . . . . . . . . ', ' . . . . . . . . ',
},
bgPattern = EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
' . . X X X X X . ',
' X . . X X X . . ',
' X X . . . . . X ',
' X X X X X X X X ',
' X X X X X X X X ',
},
yDuration = 1,
yReversed = true,
},
yDuration = 1.25,
yReversed = true,
yEase = playdate.easingFunctions.inOutSine,
yScale = 2,
xShift = 2,
alpha = 1, -- adjust to achieve translucent results shown below
}Consider the waterfall example just above. Now consider a situation in which a character may walk behind
the waterfall. You can adjust the opacity of your pattern with the alpha property, allowing some of the
content behind it to show through (even when the pattern itself is opaque). Unlike the above example where
a single transparent layer is shown above the image, here the entire multi-layer opaque result is dithered
to reveal the content behind.
Here's what the above pattern looks like when alpha is set to 0.25, 0.5, 0.75, and 1.0.
You can also change the dither type used by setting the ditherType property. Here's what it looks like with
graphics.image.kDitherTypeDiagonalLine.
You can specify an imagetable for your pattern, enabling the pattern itself to animate in addition to any easing
effects added by EasyPattern. First, here's what that looks like without any easing applied. Note the tickDuration,
which indicates how long to display each frame in the sequence:
Demo Swatch Name: dashing
EasyPattern {
pattern = gfx.imagetable.new("images/hdashes"),
tickDuration = 1/8,
}You can add easing on top of the image table animation to amplify its effect:
EasyPattern {
pattern = gfx.imagetable.new("images/hdashes"),
tickDuration = 1/8,
xDuration = 2,
ease = playdate.easingFunctions.outExpo,
reverses = true,
scale = 6,
},You can set callbacks that trigger when the pattern loops in the X axis, Y axis, or overall in order to adjust the pattern itself or trigger other effects in sync with its movement. This example adds X and Y loop callbacks to the previous Circular Pan example, adjusting the scale with each cycle in order to spiral outward then inward again repeatedly.
Demo Swatch Name: circle (copy/paste the loop callbacks below and set the starting scale to 1)
EasyPattern {
pattern = BitPattern {
' X X X X X X X X ',
' X X . . . X X X ',
' X . X X X . X X ',
' X . X X X . X X ',
' X . X X X . X X ',
' X X . . . X X X ',
' X X X X X X X X ',
' X X X X X X X X ',
},
duration = 1,
ease = playdate.easingFunctions.inOutSine,
xOffset = 2, -- half the duration
reverses = true,
scale = 1,
xLoopCallback = function(pttrn, n)
pttrn.xScale += n//5%2 == 0 and 1 or -1
end,
yLoopCallback = function(pttrn, n)
pttrn.yScale += n//5%2 == 0 and 1 or -1
end,
},Lastly, you can set an update() function on your pattern that gets called every time new phases get calculated.
This function is passed the EasyPattern itself as well as the current time, and allows you to modify the pattern
based on any inputs or game conditions you wish. This is a simple example that modifies the
"Conveyor Belt" above, allowing it to be operated with the crank.
Demo Swatch Name: conveyor (adjust the commented lines to modify the original example)
EasyPattern {
pattern = {
ditherType = playdate.graphics.image.kDitherTypeHorizontalLine,
alpha = 0.25,
},
bgColor = playdate.graphics.kColorWhite,
update = function(p) p.yShift = playdate.getCrankPosition()//15 end,
}EasyPatternDemoSwatch.lua provides a quick way to try out EasyPattern in your own
project. Just drop the file next to EasyPattern.lua, include it in main.lua, and create an instance by specifying
the ID of a pattern listed above.
local swatch = EasyPatternDemoSwatch("waves")Alternatively, you can tile the examples on screen:
EasyPatternDemoSwatch.tile()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 for succinctness; 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 specify
an optional alpha channel. BitPattern automatically swizzles the inputs, so you can 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…
}For additional convenience, BitPattern also accepts ASCII representations of these strings:
- Use
0,., or_for black/transparent pixels - Use
1,X, or any other non-black-pixel character for white/opaque pixels - Add spaces between pixels to aid legibility, if desired.
Here's the same pattern shown above in a more legible form:
BitPattern {
-- PTTRN ---------- -- ALPHA ---------
' X . X . X . X . ', ' . . . X . . . .',
' . X . X . X . X ', ' . . X X X . . .',
' X . X . X . X . ', ' . X X X X X . .',
' . X . X . X . X ', ' X X X X X X X .',
' X . X . X . X . ', ' . X X X X X . .',
' . X . X . X . X ', ' . . X X X . . .',
' X . X . X . X . ', ' . . . X . . . .',
' . X . X . X . X ', ' . . . . . . . .',
}Make sure you've specified the pattern parameter properly. More info on
defining your patterns is provided in the previous section.
- First, make sure you've specified an
xDurationand/oryDuration, without which your pattern will remain static. - If you're drawing in a sprite, ensure that
draw()gets called as necessary to reflect changes in the pattern. You can callself:markDirty()from yourupdate()function. Otherwise, just be sure to call your draw method as needed each frame. See the notes on performance to optimize drawing.
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.
EasyPattern also makes effort to ensure that no unnecessary work is done. For example, when multiple
conditions would warrant updates to the pattern image or background image, the actual update only
happens once. It also exits early from setters which modify the pattern if the value hasn't changed.
This avoids unnecessary work if, for example, you call setInverted(<condition>) every frame from
an update() callback. EasyPattern also avoids creating new temporary images to perform compositing,
keeping the memory footprint minimal and avoiding unnecessary garbage collection.
This demonstration illustrates how patterns update only as needed. The orange flashes show the regions of the screen that get redrawn each frame (toggle in the Simulator under the View menu). Note how each pattern updates on different intervals (even non-regular ones) only when it changes.
With all of that said, EasyPattern is certainly not the fastest 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.



































































