Tasmota/lib/libesp32/berry_animation/animation_docs/Animation_Tutorial.md

52 KiB

Animation DSL Tutorial

!!! tip "Try it online!" You can test all examples in this tutorial directly in your browser using the Online Emulator. No hardware required! All tutorial examples are available in the Examples dropdown menu.

This tutorial will guide you through the Berry Animation DSL, from your first solid color to complex animated sequences with templates. Each chapter builds on the previous one, introducing new concepts progressively.

Table of Contents


Overview

The Animation DSL (Domain-Specific Language) lets you create LED strip animations using simple, declarative syntax. Instead of writing complex Berry code with timing loops and state machines, you describe what you want and how long it should take.

Why Use the DSL?

Traditional LED programming requires managing frame buffers, timing loops, and complex state machines. The Animation DSL abstracts all of this away:

  • No timing code - Just specify durations like period=2s and the framework handles the rest
  • No state machines - Animations automatically manage their internal state
  • Composable - Layer multiple animations, use one as an opacity mask for another
  • Readable - Code reads almost like English: "animation pulse = breathe_animation(color=red, period=2s)"

Key Concepts

Before diving into code, let's understand the building blocks:

Concept What It Does Example
Animation A visual effect on the LED strip solid, twinkle_animation, beacon_animation
Color Either a static value or a dynamic provider that changes over time red, 0xFF0000, color_cycle(...)
Palette A collection of colors for gradients or cycling PALETTE_RAINBOW, custom arrays
Value Provider A number that changes over time (oscillates) sine_osc, triangle, sawtooth
Sequence Orchestrates animations with timing control play, wait, repeat
Template A reusable animation pattern with parameters template animation cylon_eye { ... }

How It Works

  1. You write DSL code in a .anim file
  2. The transpiler converts it to Berry code
  3. Berry code runs on your ESP32 (or in the browser emulator)
  4. The animation engine renders frames to your LED strip

Chapter 1: Getting Started

This chapter covers the basics: creating simple animations, using colors, and layering effects. By the end, you'll understand how to combine multiple animations into a scene.

1.1 Your First Animation: Solid Color

Solid Red

Every animation starts with two steps: define it, then run it. The simplest animation fills the entire strip with a single color.

# Solid red background - the simplest animation

animation back = solid(color=red)
run back

Let's break this down:

  • animation back - Creates a new animation and names it "back"
  • solid(color=red) - Uses the solid animation type, which fills all LEDs with one color
  • run back - Starts the animation running

The DSL provides many predefined colors: red, green, blue, white, yellow, orange, purple, cyan, and more.

1.2 Custom Colors

Custom Color

Predefined colors are convenient, but you'll often want specific shades. Define custom colors using hexadecimal values.

# Solid dark blue - using a custom color

color space_blue = 0x000066    # Note: opaque 0xFF alpha channel is implicitly added
animation back = solid(color=space_blue)
run back

Color format: Colors include an alpha channel (transparency) in addition to RGB:

  • 6-digit format 0xRRGGBB - The alpha is automatically set to FF (fully opaque)
  • 8-digit format 0xAARRGGBB - You specify the alpha explicitly

The alpha channel ranges from 00 (fully transparent) to FF (fully opaque). Examples:

  • 0xFF0000 = pure red (actually stored as 0xFFFF0000)
  • 0x00FF00 = pure green (actually stored as 0xFF00FF00)
  • 0x000066 = dark blue, fully opaque
  • 0x80FF0000 = semi-transparent red (50% opacity)

The DSL also provides transparent as a predefined color, equivalent to 0x00000000 (fully transparent black). This is useful for backgrounds or opacity masks where you want parts to be invisible.

1.3 Using Predefined Animations

Twinkle Stars

Beyond solid, the DSL includes many ready-to-use animation types. Each creates a different visual effect. Let's try twinkle_animation, which creates a twinkling stars effect.

# Twinkle stars - using predefined animations

animation stars = twinkle_animation()
run stars

With no parameters, animations use sensible defaults. The twinkle effect will use white sparkles at a moderate density. But you can customize everything!

1.4 Animation Parameters

Twinkle Parameters

Most animations accept parameters to customize their behavior. Parameters use the name=value syntax inside the parentheses.

# Twinkle stars with parameters - using animation parameters

# Note: when parameters are in separate lines, you don't need a comma ','
animation stars = twinkle_animation(
  color=0xFFFFAA        # Light yellow sparkles
  density=8             # density (moderate sparkles)
  twinkle_speed=100ms   # twinkle speed
  fade_speed=50         # when no unit, time unit is 'ms'  
)
run stars

!!! tip "Parameter Syntax" When parameters are on separate lines, commas are optional. This makes the code more readable for complex animations.

Time values can use units (internally converted to milliseconds):

  • 500ms - milliseconds (stays as 500)
  • 2s - seconds (converted to 2000)
  • 1m - minutes (converted to 60000)

1.5 Layering Animations

Twinkle Night

One of the most powerful features is layering - running multiple animations simultaneously. Each animation has a priority that determines its rendering order. Think of it like layers in an image editor: lower numbers are "on top" and can obscure higher numbers.

# Twinkle night - twinkle stars over a dark night

# Dark blue background
color space_blue = 0x000066
animation background = solid(color=space_blue)
run background

# Twinkle stars on top
animation stars = twinkle_animation(
  color=0xFFFFAA        # Light yellow sparkles
  density=8             # density (moderate sparkles)
  twinkle_speed=100ms   # twinkle speed
  fade_speed=50         # when no unit, time unit is 'ms'
  priority=8            # default priority is 10, so being lower puts it on top
)
run stars

Here's what happens:

  1. The background animation runs with default priority 10
  2. The stars animation runs with priority 8
  3. Since 8 < 10, stars render "on top" of the background
  4. Where there's no star, the blue background shows through

This layering technique is fundamental - you'll use it throughout this tutorial to create complex scenes.


Chapter 2: Color Cycling with Palettes

So far, our colors have been static. This chapter introduces dynamic colors that change over time, creating smooth color transitions without any manual timing code.

The key concept is the color provider - instead of a fixed color value, you use a function that produces different colors as time passes.

2.1 Built-in Palette Cycling

Palette Rotation

The simplest way to create changing colors is with color_cycle, which steps through a palette of colors over time.

# Rainbow colors cycling through the entire strip over time

# Define a color that cycles over time, cycle is 5 seconds
# PALETTE_RAINBOW_W defines 7 rainbow colors + white
color rainbow_color = color_cycle(colors=PALETTE_RAINBOW_W, period=5s)
animation back = solid(color=rainbow_color)
run back

Notice the difference from Chapter 1:

  • Before: color=red (static color)
  • Now: color=rainbow_color (dynamic color provider)

The color_cycle function creates a color provider that cycles through the palette. The period=5s means one complete cycle takes 5 seconds.

Built-in palettes:

  • PALETTE_RAINBOW - 7 rainbow colors
  • PALETTE_RAINBOW_W - 7 rainbow colors + white
  • PALETTE_RAINBOW2 - Rainbow with first color repeated at end for smooth roll-over
  • PALETTE_RAINBOW_W2 - Rainbow + white with first color repeated at end for smooth roll-over
  • PALETTE_FIRE - Fire colors (red, orange, yellow)

2.2 Custom Palettes

Custom Palette

Built-in palettes are convenient, but you'll often want your own color schemes. Define custom palettes as arrays of hex colors.

# Rainbow colors cycling with custom palette

# Define a palette of rainbow colors including white
palette rainbow_with_white = [
  0xFC0000        # Red
  0xFF8000        # Orange
  0xFFFF00        # Yellow
  0x00FF00        # Green
  0x00FFFF        # Cyan
  0x0080FF        # Blue
  0x8000FF        # Violet
  0xCCCCCC        # White
]

# Define a color that cycles over time
color rainbow_color = color_cycle(colors=rainbow_with_white, period=5s)

# Use it in a solid animation
animation back = solid(color=rainbow_color)
run back

The palette keyword creates a named color collection. Colors are listed in order - the cycle will go Red → Orange → Yellow → ... → White → Red.


Chapter 3: Smooth Color Transitions

Chapter 2 showed color_cycle, which steps discretely between colors. This chapter introduces rich_palette, which creates smooth, interpolated transitions - the color gradually morphs from one to the next.

3.1 Rich Palette Animation

Rich Palette

The rich_palette_animation is a complete animation that handles both the color transitions and rendering. It's the easiest way to get smooth rainbow effects.

# Smooth cycling through rainbow colors

animation back = rich_palette_animation()
# Equivalent to:
# animation back = rich_palette_animation(colors=PALETTE_RAINBOW, period=5s,
#                                         transition_type=SINE, brightness=100%)
run back

With no parameters, it uses sensible defaults. The transition_type=SINE creates smooth, natural-looking transitions that ease in and out.

3.2 Rich Palette with Custom Colors

Rich Palette Custom

For more control, use rich_palette as a color provider (not an animation). This lets you use smooth color transitions with any animation type.

# Smooth cycling through rainbow colors with custom palette

palette rainbow_with_white = [
  0xFC0000        # Red
  0xFF8000        # Orange
  0xFFFF00        # Yellow
  0x00FF00        # Green
  0x00FFFF        # Cyan
  0x0080FF        # Blue
  0x8000FF        # Violet
  0xCCCCCC        # White
  0xFC0000        # Red - add first color at end for smooth roll-over
]

# Define a color that cycles over time with smooth transitions
color rainbow_color_rollover = rich_palette(period=10s)

# Use the dynamic color in a solid animation
animation back = solid(color=rainbow_color_rollover)
run back

!!! tip "Smooth Roll-over" Add the first color at the end of your palette to ensure smooth transitions when the cycle repeats.


Chapter 4: Spatial Patterns and Gradients

Until now, all LEDs displayed the same color at any given moment. This chapter introduces spatial variation - different LEDs showing different colors simultaneously, creating gradients and patterns across the strip.

The key insight is that color providers can work in two dimensions:

  • Time: Colors change as time passes (what we've done so far)
  • Space: Colors vary by position along the strip (new in this chapter)

4.1 Rainbow Gradient

Color Pattern

A gradient maps colors to positions along the strip. The palette_gradient_animation does exactly this.

# Rainbow pattern across the strip

# Define a palette with period=0 (no time-based change, only spatial)
color rainbow_rich_color = rich_palette(colors=PALETTE_RAINBOW_W, period=0)

# Create a gradient across the whole strip
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color)
run back_pattern

The magic is period=0 - this tells the color provider to ignore time and only vary by position. The gradient animation then maps the palette across the strip's length.

4.2 Multiple Gradient Repetitions

Gradient 2x

By default, the gradient spans the entire strip once. Use spatial_period to control how many LEDs one complete gradient cycle covers.

# Rainbow gradient with 2 repetitions across the strip

color rainbow_rich_color = rich_palette(colors=PALETTE_RAINBOW_W, period=0)

# Get the strip length as a variable
set strip_len = strip_length()

# Create gradient with half the strip length as spatial period
animation back_pattern = palette_gradient_animation(
  color_source = rainbow_rich_color
  spatial_period = strip_len / 2
)
run back_pattern

With spatial_period = strip_len / 2, the gradient repeats twice across the strip. Use smaller values for more repetitions.

!!! note "Using strip_length()" The strip_length() function returns the current LED count. Since it's a value provider, you must assign it to a variable with set before using it in calculations.

4.3 Oscillating Spatial Period

Oscillating Period

Here's where things get interesting: you can make any parameter dynamic by using a value provider instead of a fixed number. This example makes the gradient "breathe" by oscillating its spatial period.

# Rainbow gradient with oscillating spatial period

color rainbow_rich_color = rich_palette(colors=PALETTE_RAINBOW_W, period=0)

set strip_len = strip_length()

# Oscillate spatial period between 1/2 and 3/2 of strip length
set period = sine_osc(min_value = (strip_len - 1) / 2, max_value = (3 * strip_len) / 2, duration = 5s)

animation back = palette_gradient_animation(color_source = rainbow_rich_color, spatial_period = period)
run back

The sine_osc function creates a value provider - a number that changes smoothly over time. Here it oscillates the spatial period, making the gradient compress and expand.

Available oscillators:

Oscillator Description
sine_osc Smooth sine wave
cosine_osc Cosine wave (same as sine, different phase)
triangle Linear up and down
sawtooth Linear up, instant reset
smooth Smooth cosine-based
square Instant on/off

4.4 Rotating Gradient

Rotating Gradient

Make the gradient rotate along the strip:

# Rainbow gradient rotating along the strip over 5 seconds

color rainbow_rich_color = rich_palette(colors=PALETTE_RAINBOW_W, period=0)

animation back = palette_gradient_animation(
  color_source = rainbow_rich_color
  shift_period = 5s    # Complete rotation in 5 seconds
)
run back

The shift_period parameter makes the entire pattern shift along the strip.

4.5 VU-Meter Style Animation

VU Meter

Create a meter/bar that fills based on a value:

# VU-meter style animation with green-yellow-red gradient

# Define a VU-meter palette with position-based colors
palette vue_meter_palette = [
  (  0, 0x00FF00)     # Green at 0%
  (143, 0x00FF00)     # Green until ~56%

  (164, 0xFFFF00)     # Yellow transition
  (207, 0xFFFF00)     # Yellow until ~81%

  (228, 0xFF0000)     # Red transition
  (255, 0xFF0000)     # Red at 100%
]

color rainbow_rich_color = rich_palette(colors=vue_meter_palette, period=0, transition_type=LINEAR)

# Sawtooth value from 0% to 100%
set level = sawtooth(min_value = 0%, max_value=100%, duration = 2s)

# Create the meter animation
animation back = palette_meter_animation(color_source = rainbow_rich_color, level = level)
run back

The palette uses position-based entries (position, color) where position ranges from 0 to 255.

4.6 Custom Value Functions

Random Meter

Sometimes the built-in oscillators aren't enough - you need custom logic like random values, sensor readings, or complex calculations. The DSL lets you embed native Berry code and use it in your animations.

How it works:

  1. Embed Berry code using berry """...""" blocks (triple quotes) and multi-line content
  2. Declare the function with extern function name so the DSL knows about it
  3. Call the function in your animation parameters

Function signature: Your function receives an engine parameter that provides access to timing and animation state. It should return a value (typically 0-255 for levels/opacity).

# VU-meter with random level using custom Berry function

# Step 1: Embed native Berry code
# The function receives 'engine' which has properties like time_ms
berry """
def rand_meter(engine)
  # Use time to generate pseudo-random values
  # The & 0xFF ensures result is 0-255
  return (engine.time_ms * 2654435761) & 0xFF
end
"""

# Step 2: Declare the function for DSL use
# This tells the transpiler that 'rand_meter' is a valid function
extern function rand_meter

palette vue_meter_palette = [
  (  0, 0x00FF00)     # Green
  (143, 0x00FF00)
  (164, 0xFFFF00)     # Yellow
  (207, 0xFFFF00)
  (228, 0xFF0000)     # Red
  (255, 0xFF0000)
]

color rainbow_rich_color = rich_palette(colors=vue_meter_palette, period=0, transition_type=LINEAR)

# Step 3: Use the custom function as a parameter
# Call it with () - the engine parameter is passed automatically
animation back = palette_meter_animation(color_source = rainbow_rich_color, level = rand_meter())
run back

The berry """...""" block lets you embed arbitrary Berry code. The extern function declaration makes it available to the DSL.


Chapter 5: Beacons and Moving Effects

Previous chapters filled the entire strip with colors or gradients. This chapter introduces beacons - localized highlights at specific positions. By animating the position, you create moving effects like the classic "Cylon eye" scanner.

A beacon has five key properties:

Property Description Default
color The highlight color white
back_color The background color transparent
pos Position on the strip (pixel index) 0
beacon_size Width of the highlight in pixels 1
slew_size Fade-out width on each edge (soft edges) 0

5.1 Static Beacon

Static Beacon

Let's start with a stationary beacon - a red highlight on a blue background.

# Static beacon

animation back = beacon_animation(
  back_color = blue
  color = red
  pos = 5              # Start at pixel 5
  beacon_size = 7      # 7 pixels wide
)
run back

5.2 Beacon with Slew (Soft Edges)

Beacon Slew

Hard edges can look harsh. The slew_size parameter adds a gradual fade on each side of the beacon, creating softer, more natural-looking highlights.

How slew works:

  • The slew creates a transition zone of slew_size pixels on each side of the beacon
  • Within the slew zone, the color gradually blends from color to back_color
  • The blending is linear: the first slew pixel is mostly color, the last is mostly back_color
  • If back_color is transparent, the edges become progressively transparent, allowing underlying animations to show through

Total width: A beacon with beacon_size=7 and slew_size=3 occupies 7 + 3 + 3 = 13 pixels total (7 solid + 3 fade on each side).

# Static beacon with slew

animation back = beacon_animation(
  back_color = blue
  color = red
  pos = 5
  beacon_size = 7
  slew_size = 3        # 3 pixel fade on each side
)
run back

5.3 Animated Slew

Oscillating Slew

Remember: any numeric parameter can be replaced with a value provider. Here we make the slew size pulse in and out.

# Beacon with oscillating slew

set slew = cosine_osc(min_value = 0, max_value = 4, duration = 2s)

animation back = beacon_animation(
  back_color = blue
  color = red
  pos = 5
  beacon_size = 7
  slew_size = slew     # Dynamic slew size
)
run back

5.4 Cylon Eye (Moving Beacon)

Cylon Eye

Now for the classic effect: a beacon that moves back and forth across the strip. We use cosine_osc on the pos parameter to create smooth scanning motion.

# Moving red beacon - Cylon style

set strip_len = strip_length()

animation back = beacon_animation(
    color = red
    pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 5s)
    beacon_size = 3       # small 3 pixels eye
    slew_size = 2         # with 2 pixel shading around
)
run back

The cosine oscillator creates smooth acceleration and deceleration at the ends, just like the original Battlestar Galactica Cylon!

5.5 Rainbow Cylon with Stars

Rainbow Cylon

Let's combine everything we've learned: layered animations, dynamic colors, and moving beacons. This example creates a scene with two layers.

Layer structure (rendered bottom to top):

Priority Animation Description
10 (default) stars Twinkling background - rendered first
5 back Moving rainbow beacon - rendered on top

How layering works:

  1. The stars animation runs with default priority 10
  2. The back beacon runs with priority 5 (lower = on top)
  3. The beacon uses back_color = transparent (default), so where there's no beacon, the stars show through
  4. The beacon's slew creates a soft edge that gradually reveals the stars underneath
# Moving rainbow beacon with twinkling stars background

set strip_len = strip_length()

# Twinkling stars background
animation stars = twinkle_animation(
  color=0xFFFFAA
  density=2
  twinkle_speed=100ms
  fade_speed=100
  # priority = 10 (default)
)
run stars

# Moving beacon with dynamic color
# back_color defaults to transparent, so stars show through
animation back = beacon_animation(
    color = rich_palette(colors=PALETTE_RAINBOW_W2, period=5s)
    pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 5s)
    beacon_size = 3
    slew_size = 2
    priority = 5          # Lower priority = rendered on top of stars
)
run back

The result: a rainbow-colored eye scans across a field of twinkling stars, with the stars visible everywhere except where the beacon is.

5.6 Beacon as Opacity Mask

Opacity Mask

Instead of layering animations with priority, you can use one animation as an opacity mask for another. This creates a "window" effect where the mask controls what's visible.

How opacity masks work:

  1. The mask animation renders to determine opacity values (0-255 per pixel)
  2. Where the mask is bright (white/high values), the main animation is fully visible
  3. Where the mask is dark (black/low values), the main animation is transparent
  4. The mask's actual color doesn't matter - only its brightness/alpha is used

In this example:

  • moving_eye is a beacon that moves back and forth
  • eye_pattern is a red-blue-red gradient across the entire strip
  • The gradient is only visible where the beacon is - creating a "spotlight" effect
  • The beacon's slew creates soft edges on the spotlight
# Moving beacon used as opacity filter on pattern

set strip_len = strip_length()

# Define a red-blue-red gradient palette
palette red_blue_red_palette = [ red, 0x3333FF, red ]
color red_blue_red_color = rich_palette(colors=red_blue_red_palette)

# Moving beacon as opacity mask
# The color is white but it doesn't matter - only brightness counts
animation moving_eye = beacon_animation(
    color = white           # Color doesn't matter, only brightness/alpha
    pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 5s)
    beacon_size = 3
    slew_size = 2
)

# Apply the mask to a gradient
# The gradient exists everywhere, but only shows where the beacon is
animation eye_pattern = palette_gradient_animation(
  color_source = red_blue_red_color
  opacity = moving_eye      # Use beacon as opacity mask
)
run eye_pattern

The result: a moving "window" reveals different parts of the underlying gradient as it scans across the strip.


Chapter 6: Shutters and Sequences

This chapter introduces two powerful concepts:

  1. Shutters - Expanding/contracting effects created by animating a beacon's size
  2. Sequences - Orchestrating multiple animations with precise timing control

Sequences are essential when you need things to happen in order: play animation A, then wait, then play animation B, then change a color, etc.

6.1 Simple Shutter

Simple Shutter

A shutter effect is simply a beacon with an animated size. Using sawtooth makes it grow linearly then reset.

# Shutter left to right using beacon

set strip_len = strip_length()

# Sawtooth from 0 to strip_len - grows linearly, then resets
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = 1.5s)

animation shutter_lr_animation = beacon_animation(
    color = red
    back_color = blue
    pos = 0                         # Start from left
    beacon_size = shutter_size      # Expanding size
)
run shutter_lr_animation

6.2 Shutter with Rotating Colors

Rotating Shutter

Now let's add color changes after each shutter cycle. This requires a sequence - a way to run code at specific times.

A sequence contains steps that execute in order:

  • play animation for duration - Run an animation for a specific time
  • wait duration - Pause before the next step
  • restart provider - Reset a value provider's timing
  • repeat N times { ... } - Loop a block of steps
# Shutter with rotating colors using sequence

set strip_len = strip_length()
set period = 1.5s

set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)

# Two color providers cycling through rainbow
# period=0 means they don't auto-cycle - we control them manually
color col1 = color_cycle(colors=PALETTE_RAINBOW_W, period=0)
color col2 = color_cycle(colors=PALETTE_RAINBOW_W, period=0)
col2.next = 1           # Shift col2 by one color at startup

animation shutter_lr_animation = beacon_animation(
    color = col2
    back_color = col1
    pos = 0
    beacon_size = shutter_size
)

# Sequence running forever
sequence shutter_seq repeat forever {
    restart shutter_size        # Sync timing
    play shutter_lr_animation for period
    col1.next = 1               # Advance col1 to next color in palette
    col2.next = 1               # Advance col2 to next color in palette
}
run shutter_seq

Understanding color_cycle with period=0:

When period=0, the color provider doesn't automatically cycle through colors over time. Instead, you control it manually by writing to its next property:

  • col.next = 1 - Advance to the next color in the palette
  • col.next = 2 - Skip ahead by 2 colors
  • col.next = -1 - Go back to the previous color

This gives you precise control over color changes, synchronized with your sequence timing.

Sequence statements:

Statement Description
play animation for duration Play an animation for a specific time
wait duration Pause the sequence
restart value_provider Reset a value provider's timing
repeat N times { ... } Repeat a block N times
repeat forever { ... } Repeat indefinitely

6.3 Central Shutter

Central Shutter

Instead of expanding from the left edge, this shutter expands from the center outward.

From left-right to center-out:

The key difference is the pos calculation:

  • Left-right shutter (6.1): pos = 0 - beacon starts at left edge and grows rightward
  • Center-out shutter (6.3): pos = strip_len_2 - (shutter_size + 1) / 2 - beacon stays centered as it grows

The formula strip_len_2 - (shutter_size + 1) / 2 keeps the beacon centered by:

  1. Starting at the center (strip_len_2)
  2. Subtracting half the beacon size so it expands equally in both directions
  3. As shutter_size grows from 0 to strip_len, the position moves left to keep the beacon centered
# Shutter central with rotating colors

set strip_len = strip_length()
set strip_len_2 = (strip_len + 1) / 2       # Half length (center position)

set period = 1.5s
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)

color col1 = color_cycle(colors=PALETTE_RAINBOW_W, period=0)
color col2 = color_cycle(colors=PALETTE_RAINBOW_W, period=0)
col2.next = 1

# Position calculated to center the beacon as it grows
animation shutter_inout_animation = beacon_animation(
    color = col2
    back_color = col1
    pos = strip_len_2 - (shutter_size + 1) / 2
    beacon_size = shutter_size
)

sequence shutter_seq repeat forever {
    restart shutter_size
    play shutter_inout_animation for period
    col1.next = 1
    col2.next = 1
}
run shutter_seq

6.4 Bidirectional Shutter

Bidirectional Shutter

This example combines both directions: first the shutter expands from center (in-out), then contracts back to center (out-in). The sequence cycles through all palette colors in each direction before switching.

Sequence structure:

The sequence contains two nested repeat blocks:

  1. First repeat: Runs the in-out animation once for each color in the palette
  2. Second repeat: Runs the out-in animation once for each color in the palette

The repeat count col1.palette_size dynamically gets the number of colors in the palette (8 for PALETTE_RAINBOW_W), so the animation automatically adapts if you change palettes.

Why restart is important:

The shutter_size sawtooth oscillator runs continuously based on elapsed time. Without restart, each iteration would start wherever the sawtooth happened to be, causing visual glitches. By calling restart shutter_size at the beginning of each iteration:

  • The sawtooth resets to its starting value (0)
  • The shutter animation starts cleanly from the beginning
  • Everything stays perfectly synchronized with the sequence timing
# Shutter central in-out and out-in with rotating colors

set strip_len = strip_length()
set strip_len_2 = (strip_len + 1) / 2

set period = 1.5s
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)

color col1 = color_cycle(colors=PALETTE_RAINBOW_W, period=0)
color col2 = color_cycle(colors=PALETTE_RAINBOW_W, period=0)
col2.next = 1

# In-out animation: beacon grows from center outward
animation shutter_inout_animation = beacon_animation(
    color = col2
    back_color = col1
    pos = strip_len_2 - (shutter_size + 1) / 2
    beacon_size = shutter_size
)

# Out-in animation: beacon shrinks from edges toward center
# Uses inverted size (strip_len - shutter_size) and swapped colors
animation shutter_outin_animation = beacon_animation(
    color = col1
    back_color = col2
    pos = strip_len_2 - (strip_len - shutter_size + 1) / 2
    beacon_size = strip_len - shutter_size
)

sequence shutter_seq repeat forever {
    # First: cycle through all colors in-out (8 iterations for PALETTE_RAINBOW_W)
    repeat col1.palette_size times {
        restart shutter_size        # Reset sawtooth to 0 for clean start
        play shutter_inout_animation for period
        col1.next = 1               # Advance both colors
        col2.next = 1
    }
    # Then: cycle through all colors out-in (8 more iterations)
    repeat col1.palette_size times {
        restart shutter_size        # Reset sawtooth again
        play shutter_outin_animation for period
        col1.next = 1
        col2.next = 1
    }
}
run shutter_seq

Chapter 7: Crenel Patterns

Crenels create repeating square wave patterns - alternating blocks of two colors, like the battlements on a castle wall. They're useful for:

  • Creating striped patterns
  • Building opacity masks with regular gaps
  • Marquee-style effects when animated

7.1 Static Crenel

Static Crenel

A basic crenel alternates between two colors with configurable block sizes.

Crenel parameters:

Parameter Description Default
color Color of the "high" pulses white
back_color Color of the "low" gaps transparent (0x00000000)
pos Starting position on the strip 0
pulse_size Width of each pulse in pixels 1
low_size Width of each gap in pixels 3
nb_pulse Number of pulses (-1 = infinite) -1

The pattern repeats with a period of pulse_size + low_size pixels.

# Static crenel pattern

animation back = crenel_animation(
    color = red
    back_color = blue
    pulse_size = 2        # 2 pixels of 'color'
    low_size = 2          # 2 pixels of 'back_color'
)
run back

This creates a pattern: 🔴🔴🔵🔵🔴🔴🔵🔵... across the entire strip.

7.2 Variable Number of Pulses

Variable Pulses

Instead of showing all pulses (nb_pulse = -1), you can animate the number of visible pulses using a value provider. The triangle oscillator smoothly varies nb_pulse from 0 (no pulses) to the maximum that fits on the strip.

To compute the maximum number of pulses, use the formula: max_pulses = (strip_len + period - 1) / period where period = pulse_size + low_size. This ceiling division ensures we count partial pulses at the end of the strip.

# Crenel with variable number of pulses

set strip_len = strip_length()
set period = 4                    # pulse_size (2) + low_size (2)
set max_pulses = (strip_len + period - 1) / period

set nb_pulse = triangle(min_value = 0, max_value = max_pulses, duration = 2s)

animation back = crenel_animation(
    color = red
    back_color = blue
    pulse_size = 2
    low_size = 2
    nb_pulse = nb_pulse
)
run back

7.3 Variable Pulse Size

Variable Size

Instead of a fixed pulse_size, you can use a value provider to animate the pulse width over time. Here, a triangle oscillator smoothly varies the pulse size between 0 and 4 pixels over 2 seconds, creating a breathing effect on the crenel pattern:

# Crenel with variable pulse size

set pulse_size = triangle(min_value = 0, max_value = 4, duration = 2s)

animation back = crenel_animation(
    color = red
    back_color = blue
    pulse_size = pulse_size
    low_size = 2
)
run back

7.4 Dynamic Colors

Dynamic Colors

The color parameter also accepts a color provider instead of a static color. This example uses rich_palette to cycle through rainbow colors over 5 seconds, making the crenel pulses continuously change color while the blue background remains fixed:

# Crenel with dynamic color

color rainbow_color = rich_palette(colors=PALETTE_RAINBOW_W2, period=5s)

animation back = crenel_animation(
    color = rainbow_color
    back_color = blue
    pulse_size = 2
    low_size = 2
)
run back

7.5 Crenel as Opacity Mask

Crenel Mask

Instead of using a crenel directly as a visible animation, you can use it as an opacity mask for another animation. This creates a "window" effect where the crenel pattern controls what's visible.

How opacity masks work:

  1. The mask animation renders to determine opacity values (0-255 per pixel)
  2. Where the mask is bright (white/high values), the main animation is fully visible
  3. Where the mask is dark or transparent, the main animation is hidden
  4. The mask's actual color doesn't matter - only its brightness/alpha is used

In this example:

  • back is a solid blue background at priority 20
  • mask is a crenel pattern with white pulses and transparent gaps
  • pattern is a rotating rainbow gradient that uses the crenel as its opacity parameter
  • The rainbow gradient is only visible where the crenel pulses are white - creating alternating "windows" of rainbow color on a blue background
# Crenel used as opacity mask

# Blue background
animation back = solid(color = blue, priority = 20)
run back

# Crenel mask (white = visible, transparent = hidden)
animation mask = crenel_animation(
    color = white
    back_color = transparent
    pulse_size = 2
    low_size = 2
)

# Rainbow gradient masked by crenel
color rainbow_rich_color = rich_palette(colors=PALETTE_RAINBOW_W, period=0)
animation pattern = palette_gradient_animation(
    color_source = rainbow_rich_color
    shift_period = 2s           # Rotating gradient
    opacity = mask              # Apply crenel mask
)
run pattern

Chapter 8: Templates for Reusable Animations

As your animations grow more complex, you'll want to reuse patterns with different parameters. Templates solve this by letting you define animation "blueprints" with configurable parameters.

Think of templates like functions in programming: define once, use many times with different inputs.

8.1 Simple Template: Cylon Eye

Template Cylon

The template animation keyword creates a new animation type that can be instantiated just like built-in animations (solid, beacon_animation, etc.). Once defined, you use it by calling animation my_anim = template_name(param1=value1, ...) - exactly like native animations.

Defining parameters:

Each param declaration creates a configurable input for your template:

param parameter_name type type_name default default_value
  • parameter_name: The name you'll use inside the template and when instantiating
  • type type_name: Optional type constraint (see table below)
  • default value: Optional default value if not provided at instantiation

Available parameter types:

Type Description Example
color Color value (hex or named) param fg type color default red
palette Palette definition (bytes) param colors type palette
time Time value with unit (converted to ms) param period type time default 5s
int Integer value param count type int default 10
bool Boolean (true/false) param enabled type bool default true
string String value param name type string default "anim"
percentage Percentage (0-255) param brightness type percentage default 255
number Generic numeric param value type number
any Any type (no validation) param data type any

Let's package the Cylon eye effect from Chapter 5 as a reusable template. Users can customize the color, speed, and priority without rewriting the animation logic:

# Template for Cylon-style scanning eye

template animation cylon_eye {
  param eye_color type color default red
  param back_color type color default transparent
  param period type time default 5s
  param priority default 5

  set strip_len = strip_length()

  animation eye_animation = beacon_animation(
    color = eye_color
    back_color = back_color
    pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = period)
    beacon_size = 3
    slew_size = 2
    priority = priority
  )

  run eye_animation
}

# Use the template with defaults
animation eye = cylon_eye()
run eye

8.2 Template with Palette Parameter

Template Color Cycle

Templates can accept complex types like palettes. Here we create a reusable color cycling animation where the user provides their own palette and cycle period:

# Template for color cycling animation

template animation color_cycle2 {
    param colors type palette
    param period default 5s

    color rainbow_color = color_cycle(colors=colors, period=period)
    animation back = solid(color=rainbow_color)
    run back
}

# Define a custom palette
palette rgb = [
  0xFC0000        # Red
  0x00FF00        # Green
  0x0080FF        # Blue
]

# Use the template with custom parameters
animation main = color_cycle2(colors = rgb, period = 2s)
run main

8.3 Advanced Template with Conditional Flags

Template Shutter

Templates support bool parameters that can be used with if statements inside sequences. This allows users to enable or disable parts of the animation at instantiation time. Here we create a bidirectional shutter (based on chapter 6.4) that can optionally run in-out, out-in, or both directions.

Dynamic parameter changes:

You can also modify template parameters at runtime using Berry code. Note that DSL variable names get an underscore suffix in Berry to avoid collisions with reserved words (e.g., main becomes main_). For example: main_.inout = false disables the in-out animation while it's running.

# Template to illustrate the parameters and flags

# Define a template to package the shutter in-out-in from 6.40
# with flags to enable or disable in-out or out-in
template animation shutter_bidir {
  param colors type palette
  param period default 2s
  param inout type bool default true    # define to true to enable 'inout' part
  param outin type bool default true    # define to true to enable 'outin' part

  # since 'strip_length()' is a value provider, it must be assigned to a variable before being used
  set strip_len = strip_length()
  set strip_len_2 = (strip_len + 1) / 2       # half length rounded

  # Define animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
  set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)

  # Define two rotating palettes, shifted by one color
  color col1 = color_cycle(colors=colors, period=0)
  color col2 = color_cycle(colors=colors, period=0)
  col2.next = 1     # move 'col2' to the next color so it's shifte by one compared to 'col1'

  # Shutter moving in in-out
  animation shutter_inout_animation = beacon_animation(
    color = col2                    # Use two rotating colors
    back_color = col1               # Use two rotating colors
    pos = strip_len_2 - (shutter_size + 1) / 2
    beacon_size = shutter_size
  )

  # shutter moving in out-in
  animation shutter_outin_animation = beacon_animation(
    color = col1
    back_color = col2
    pos = strip_len_2 - (strip_len - shutter_size + 1) / 2
    beacon_size = strip_len - shutter_size
  )

  # this is the overall sequence composed of two sub-sequences
  # the first in ascending mode, the second in descending
  sequence shutter_seq repeat forever {
    if inout {                              # un only if 'ascending' is true
      repeat col1.palette_size times {          # run the shutter animation
        restart shutter_size                    # resync all times for this animation, to avoid temporal drift
        play shutter_inout_animation for period    # run the animation
        col1.next = 1                           # then move to next color for both palettes
        col2.next = 1
      }
    }
    if outin {                             # run only if 'descending' is true
      repeat col1.palette_size times {
        restart shutter_size
        play shutter_outin_animation for period
        col1.next = 1
        col2.next = 1
      }
    }
  }
  run shutter_seq
}

# define a palette of rainbow colors including white with constant brightness
palette rainbow_with_white = [
  0xFC0000        # Red
  0xFF8000        # Orange
  0xFFFF00        # Yellow
  0x00FF00        # Green
  0x00FFFF        # Cyan
  0x0080FF        # Blue
  0x8000FF        # Violet
  0xCCCCCC        # White
]

animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
run main

Quick Reference Card

Animation Types

Animation Description Key Parameters
solid Solid color fill color
twinkle_animation Twinkling stars effect color, density, twinkle_speed, fade_speed
beacon_animation Positioned pulse/highlight color, pos, beacon_size, slew_size
crenel_animation Square wave pattern color, back_color, pulse_size, low_size
rich_palette_animation Smooth palette cycling colors, period, transition_type
palette_gradient_animation Gradient across strip color_source, spatial_period, shift_period
palette_meter_animation VU-meter style bar color_source, level
breathe_animation Breathing/pulsing effect color, period
comet_animation Moving comet with tail color, tail_length, speed

Colors

# Predefined
red, green, blue, white, yellow, orange, purple, cyan, transparent

# Custom hex
color my_color = 0xRRGGBB

Palettes

# Built-in
PALETTE_RAINBOW, PALETTE_RAINBOW_W, PALETTE_FIRE

# Custom
palette my_palette = [ 0xFF0000, 0x00FF00, 0x0000FF ]

# Position-based
palette gradient = [ (0, red), (128, yellow), (255, green) ]

Value Providers

sine_osc(min_value=0, max_value=255, duration=2s)
cosine_osc(min_value=0, max_value=255, duration=2s)
triangle(min_value=0, max_value=255, duration=2s)
sawtooth(min_value=0, max_value=255, duration=2s)
smooth(min_value=0, max_value=255, duration=2s)
square(min_value=0, max_value=255, duration=2s)
strip_length()

Time Units

500ms    # milliseconds
2s       # seconds
1m       # minutes

Percentages

0%       # 0
50%      # 128
100%     # 255

Sequence Statements

play animation for 5s
wait 1s
restart value_provider
repeat 3 times { ... }
repeat forever { ... }
if condition { ... }

Template Definition

template animation name {
  param param_name type type_name default value
  # ... body ...
}

Resources