Skip to content

ebeneliason/easy-pattern

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

106 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EasyPattern

MIT License Toybox Compatible Latest Version

Easy animated patterns for Playdate.

Easy Pattern Demo Animation

What is EasyPattern?

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.

Table of Contents

  1. Installation
  2. Basic Usage
  3. Pattern Gallery
  4. Understanding EasyPattern
  5. Parameter Reference
  6. Function Reference
  7. Examples
  8. Demo Swatch
  9. Defining Patterns
  10. Troubleshooting & Performance

Installation

Installing Manually

  1. Download the EasyPattern.lua file.

  2. Place the file in your project directory (e.g. in the source directory next to main.lua).

  3. Import it in your project.

    import "EasyPattern"

Installing with Toybox

  1. If you haven't already, download and install toybox.py.

  2. Add EasyPattern to your project directory:

    toybox add ebeneliason/easy-pattern
    toybox update
  3. Then, if your code is in the source directory, import it as follows:

    import '../toyboxes/toyboxes.lua'

Basic Usage

Define Your Pattern

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.

Draw With Your Pattern

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 APIs

That'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).

Detect Changes in Your Pattern

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()
end

Gallery

Click 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!

Conveyor Thumbnail Scanline Thumbnail Ooze Thumbnail Marching Ants Over Image Thumbnail Bounce Thumbnail Waves Thumbnail Circular Pan Thumbnail Sway Thumbnail Vibrate Thumbnail Perlin Thumbnail Dot Matrix Thumbnail Blink Thumbnail Steam Thumbnail Reflected Thumbnail Ooze Over Image Thumbnail Ooze Over Ooze Image Thumbnail Waterfall 75% Thumbnail Dashing Thumbnail Spiral Thumbnail Crank Conveyor Thumbnail

Understanding EasyPattern

EasyPatterns can be thought of in two parts:

  1. Pattern: A collection of properties that define its overall appearance
  2. 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.

Types and Compatibility

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.easingFunctions format. You can use these functions directly, reference another library, or define custom functions of your own.

Pattern Composition

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
Loading

↓ BOTTOM

A few notes about pattern composition:

  1. Because the bgPattern property may be set to another EasyPattern instance, it's possible to create compositions which combine two or more patterns together as one.

  2. Patterns may apply an opacity effect via the alpha and ditherType properties, which applies to all layers of the pattern it's defined on and any nested more deeply beneath it.

  3. 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().

Animation Timing

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 // 1

First, 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 xReverses or yReverses to cause the animation to reverse directions at each end. The xReversed/yReversed boolean properties will flip with each reversal.
  • Reversed: Set xReversed or yReversed to cause the animation to run in the opposite direction. This may be used with or without "reverses".

Pattern Transformations

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 xReflected and yReflected properties to mirror the fully-composited pattern in the horizontal and vertical axes, respectively.
  • Rotation: Set the rotated property to rotate the fully-composited pattern by 90º, producing an orthogonal result.
  • Translation/Phase: Set the xShift and yShift properties 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 in setPhaseShifts.
  • Inversion: Set the inverted property to cause all white pixels to appear black, and vice-versa.

Supported Parameters

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:

  1. Pattern Parameters: Define the overall appearance of your pattern.
  2. Animation Parameters: Define the easing behaviors of your pattern.
  3. Transformation Parameters: Apply simple transformations to your pattern, such as translation, reflection, and rotation.
  4. 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.5

Pattern Parameters

pattern

An 8x8 pixel pattern specified in one of these formats:

  1. 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)

  2. Dither Pattern: A table containing:

    • A ditherType (as would be passed to playdate.graphics.setDitherPattern().
    • An optional alpha value in the range [0,1]. Default: 0.5.
    • An optional color value 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.

  3. Image: An 8x8 pixel playdate.graphics.image.

    Example: pattern = playdate.graphics.image.new("images/myPattern")

  4. 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.

bgPattern

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

bgColor

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

alpha

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

ditherType

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

inverted

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

tickDuration

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()

Animation Parameters

xEase

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.

yEase

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

xEaseArgs

A list containing any additional arguments to the X axis easing function, e.g. to parameterize amplitude, period, overshoot, etc.

Default: {}

yEaseArgs

A list containing any additional arguments to the Y axis easing function, e.g. to parameterize amplitude, period, overshoot, etc.

Default: {}

xDuration

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

yDuration

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

xOffset

An absolute time offset for the X axis animation, in seconds.

Default: 0

yOffset

An absolute time offset for the Y axis animation, in seconds.

Default: 0

xReverses

A boolean indicating whether the X axis animation reverses at each end.

Default: false

yReverses

A boolean indicating whether the Y axis animation reverses at each end.

Default: false

xReversed

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

yReversed

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

xSpeed

A multiplier for the overall speed of the animation in the X axis.

Default: 1

ySpeed

A multiplier for the overall speed of the animation in the Y axis.

Default: 1

xScale

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.

yScale

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.

Transformation Parameters

xReflected

A boolean indicating whether the entire pattern should be reflected across the vertical (Y) axis. See Reflected Patterns for an example.

Default: false

yReflected

A boolean indicating whether the entire pattern should be reflected across the horizontal (X) axis. See Reflected Patterns for an example.

Default: false

rotated

A boolean indicating whether the entire pattern should be rotated 90º, producing an orthogonal result. Rotation is applied following any reflections.

Default: false

xShift

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

yShift

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

Callback Parameters

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:

  1. The time between loop callbacks may not be exact, especially if the frame rate is lower.
  2. The callbacks will not be called at all if the pattern is not being used.

loopCallback

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)
end

Default: nil

xLoopCallback

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

yLoopCallback

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

update

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 end

See Dynamic Patterns for a complete example with visual.

Functions

Core Functions

init(params)

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,
}

apply()

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: A playdate.graphics.image containing 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.

isDirty()

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()
end

Note

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.

getPhases()

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.

copy([overrides])

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 to init().

Returns:

  • copy: The EasyPattern copy.

Pattern Functions

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.

setPattern(pattern)

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 to playdate.graphics.setPattern().

setPattern(ditherType, [alpha], [color])

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 by playdate.graphics.setDitherPattern().
  • [alpha]: The opacity to render the dither pattern with. Default: 0.5.
  • [color]: An optional playdate.graphics color to render the pattern in. Default: playdate.graphics.kColorBlack.

setPattern(image)

Sets the pattern using the provided image.

Params:

  • image: A playdate.graphics.image, which should be 8x8 pixels in size.

setPattern(imageTable, [tickDuration])

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: A playdate.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 the tickDuration property 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.

setBackgroundPattern(pattern)

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 to playdate.graphics.setPattern().

setBackgroundPattern(ditherType, [alpha], [color])

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 by playdate.graphics.setDitherPattern().
  • [alpha]: The opacity to render the dither pattern with. Default: 0.5.
  • [color]: An optional playdate.graphics color to render the pattern in. Default: playdate.graphics.kColorBlack.

setBackgroundPattern(image)

Sets the background pattern using the provided image.

Params:

  • image: A playdate.graphics.image, which should be 8x8 pixels in size.

setBackgroundPattern(imageTable, [tickDuration])

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: A playdate.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 the tickDuration property 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.

setBackgroundPattern(easyPattern)

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: An EasyPattern instance to use as a background.

setBackgroundColor(color)

Sets the background color shown behind any transparent areas in the pattern and bgPattern.

Params:

  • color: A playdate.graphics color constant.

setAlpha(alpha, [ditherType])

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 by playdate.graphics.setDitherPattern().

setInverted(flag)

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: A boolean indicating whether the pattern is inverted.

Transformation Functions

setPhaseShifts(xShift, [yShift])

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:

  1. A conveyor belt pattern which animates according to the crank speed
  2. 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
  3. 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.

Driftpin Example

Params:

  • xShift: The phase shift in the X axis
  • yShift: 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.

shiftPhasesBy(xShift, [yShift])

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 axis
  • yShift: The amount to to shift the phase by in the Y axis

setReflected(horizontal, [vertical])

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: A boolean indicating whether the pattern is reflected horizontally across the Y axis.
  • vertical: A boolean indicating whether the pattern is reflected vertically across the X axis.

setRotated(flag)

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.

Looping Functions

getLoopDuration()

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.

getXLoopDuration()

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.

getYLoopDuration()

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.

Examples

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.

Conveyor Belt

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:

  1. Specifying the horizontal dither type and reversed as needed
  2. Using a combination of reflected and rotated to reorient the pattern

Conveyor Checkerboard Example Conveyor Example

Conveyor Example Zoomed

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.

Scanline

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.

Scanline Checkerboard Example Scanline Over Image Example Scanline Example

Scanline Over Image Example Zoomed

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.

Ooze

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.

Ooze Checkerboard Example Ooze Example

Ooze Example Zoomed

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,
},

Marching Ants

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().

Marching Ants Checkerboard Example Marching Ants Example Marching Ants Over Image Example

Marching Ants Over Image Example Zoomed

Demo Swatch ID: ants

EasyPattern {
    pattern   = playdate.graphics.image.kDitherTypeDiagonalLine,
    xDuration = 0.25,
    bgColor   = playdate.graphics.kColorWhite
}

Vertical Bounce

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.

Bounce Checkerboard Example Bounce Example

Bounce Example Zoomed

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,
}

Waves

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.

Waves Checkerboard Example Waves Example

Waves Example Zoomed

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,
}

Circular Pan

This example makes use of built-in sine functions and an xOffset to create a continuous circular panning movement.

Circular Pan Checkerboard Example Circular Pan Example

Circular Pan Example Zoomed

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,
}

Sway

Changing just a few parameters from the above example creates a gentle swaying motion.

Sway Checkerboard Example Sway Example

Sway Example Zoomed

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,
}

Vibrate

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.

Vibrate Checkerboard Example Vibrate Example

Vibrate Example Zoomed

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.

Perlin Noise

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.

Perlin Checkerboard Example Perlin Example

Perlin Example Zoomed

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
}

Dot Matrix

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.

Dot Matrix Checkerboard Example Dot Matrix Example

Dot Matrix Example Zoomed

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,
}

Blink

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.

Blink Example

Blink Example Zoomed

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
}

Steam

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.

Steam Checkerboard Example Steam Example Steam Over Image Example

Steam Over Image Example Zoomed

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,
}

Reflected Patterns

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.)

Reflected Checkerboard Example Reflected Example

Reflected Example Zoomed

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(...)

Composite Patterns

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.

Waterfall Pattern Example Waterfall Background Example Waterfall Composite Example

Waterfall Example Zoomed Waterfall Over Image Example Zoomed

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
}

Translucent Patterns

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.

Translucent Waterfall 25% Example Zoomed Translucent Waterfall 50% Example Zoomed Translucent Waterfall 75% Example Zoomed Translucent Waterfall 100% Example Zoomed

You can also change the dither type used by setting the ditherType property. Here's what it looks like with graphics.image.kDitherTypeDiagonalLine.

Translucent Waterfall Diagonal Dither Example Zoomed

Animated Patterns

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,
}

Dashing Checkerboard Without Ease Example Dashing Without Ease Example

Dashing Without Ease Example Zoomed

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,
},

Dashing Checkerboard Example Dashing Example

Dashing Example Zoomed

Self-Mutating Patterns

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.

Spiral Checkerboard Example Spiral Example

Spiral Example Zoomed

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,
},

Dynamic Patterns

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.

Crank Conveyor Example Zoomed

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,
}

Demo Swatch

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()

Defining Your Patterns

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 ',  ' . . . . . . . .',
}

Troubleshooting

What if my pattern doesn't appear?

Make sure you've specified the pattern parameter properly. More info on defining your patterns is provided in the previous section.

What if my pattern doesn't animate?

  1. First, make sure you've specified an xDuration and/or yDuration, without which your pattern will remain static.
  2. If you're drawing in a sprite, ensure that draw() gets called as necessary to reflect changes in the pattern. You can call self:markDirty() from your update() function. Otherwise, just be sure to call your draw method as needed each frame. See the notes on performance to optimize drawing.

What About Performance?

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.

Pattern Update Visualization

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.

License

EasyPattern is distributed under the terms of the MIT License.

About

Easy animated patterns for Playdate

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages