Skip to content

User-Defined Functions~

Create custom animation functions in Berry and use them seamlessly in the Animation DSL.

Quick Start~

1. Create Your Function~

Write a Berry function that creates and returns an animation:

# Define a custom breathing effect
def my_breathing(engine, color, speed)
  var anim = animation.pulsating_animation(engine)
  anim.color = color
  anim.min_brightness = 50
  anim.max_brightness = 255
  anim.period = speed
  return anim
end

2. Register It~

Make your function available in DSL:

animation.register_user_function("breathing", my_breathing)

3. Use It in DSL~

First, import your user functions module, then call your function directly in computed parameters:

# Import your user functions module
import user_functions

# Use your custom function in computed parameters
animation calm = solid(color=blue)
calm.opacity = breathing_effect()

animation energetic = solid(color=red) 
energetic.opacity = breathing_effect()

sequence demo {
  play calm for 10s
  play energetic for 5s
}

run demo

Importing User Functions~

DSL Import Statement~

The DSL supports importing Berry modules using the import keyword. This is the recommended way to make user functions available in your animations:

# Import user functions at the beginning of your DSL file
import user_functions

# Now user functions are available directly
animation test = solid(color=blue)
test.opacity = my_function()

Import Behavior~

  • Module Loading: import user_functions transpiles to Berry import "user_functions"
  • Function Registration: The imported module should register functions using animation.register_user_function()
  • Availability: Once imported, functions are available throughout the DSL file
  • No Compile-Time Checking: The DSL doesn't validate user function existence at compile time

Example User Functions Module~

Create a file called user_functions.be:

import animation

# Define your custom functions
def rand_demo(engine)
  import math
  return math.rand() % 256  # Random value 0-255
end

def breathing_effect(engine, base_value, amplitude)
  import math
  var time_factor = (engine.time_ms / 1000) % 4  # 4-second cycle
  var breath = math.sin(time_factor * math.pi / 2)
  return int(base_value + breath * amplitude)
end

# Register functions for DSL use
animation.register_user_function("rand_demo", rand_demo)
animation.register_user_function("breathing", breathing_effect)

print("User functions loaded!")

Using Imported Functions in DSL~

import user_functions

# Simple user function call
animation random_test = solid(color=red)
random_test.opacity = rand_demo()

# User function with parameters
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = breathing(128, 64)

# User functions in mathematical expressions
animation complex = solid(color=green)
complex.opacity = max(50, min(255, rand_demo() + 100))

run random_test

Multiple Module Imports~

You can import multiple modules in the same DSL file:

import user_functions      # Basic user functions
import fire_effects       # Fire animation functions
import color_utilities    # Color manipulation functions

animation base = solid(color=random_color())
base.opacity = breathing(200, 50)

animation flames = solid(color=red)
flames.opacity = fire_intensity(180)

Common Patterns~

Simple Color Effects~

def solid_bright(engine, color, brightness_percent)
  var anim = animation.solid_animation(engine)
  anim.color = color
  anim.brightness = int(brightness_percent * 255 / 100)
  return anim
end

animation.register_user_function("bright", solid_bright)
animation bright_red = solid(color=red)
bright_red.opacity = bright(80)

animation dim_blue = solid(color=blue)
dim_blue.opacity = bright(30)

Fire Effects~

def custom_fire(engine, intensity, speed)
  var color_provider = animation.rich_palette(engine)
  color_provider.colors = animation.PALETTE_FIRE
  color_provider.period = speed

  var fire_anim = animation.filled(engine)
  fire_anim.color_provider = color_provider
  fire_anim.brightness = intensity
  return fire_anim
end

animation.register_user_function("fire", custom_fire)
animation campfire = solid(color=red)
campfire.opacity = fire(200, 2000)

animation torch = solid(color=orange)
torch.opacity = fire(255, 500)

Twinkling Effects~

def twinkles(engine, color, count, period)
  var anim = animation.twinkle_animation(engine)
  anim.color = color
  anim.count = count
  anim.period = period
  return anim
end

animation.register_user_function("twinkles", twinkles)
animation stars = solid(color=white)
stars.opacity = twinkles(12, 800ms)

animation fairy_dust = solid(color=0xFFD700)
fairy_dust.opacity = twinkles(8, 600ms)

Position-Based Effects~

def pulse_at(engine, color, position, width, speed)
  var anim = animation.beacon_animation(engine)
  anim.color = color
  anim.position = position
  anim.width = width
  anim.period = speed
  return anim
end

animation.register_user_function("pulse_at", pulse_at)
animation left_pulse = solid(color=green)
left_pulse.position = pulse_at(5, 3, 2000)

animation right_pulse = solid(color=blue)
right_pulse.position = pulse_at(25, 3, 2000)

Advanced Examples~

Multi-Layer Effects~

def rainbow_twinkle(engine, base_speed, twinkle_density)
  # Create base rainbow animation
  var rainbow_provider = animation.rich_palette(engine)
  rainbow_provider.colors = animation.PALETTE_RAINBOW
  rainbow_provider.period = base_speed

  var base_anim = animation.filled(engine)
  base_anim.color_provider = rainbow_provider
  base_anim.priority = 1

  # Note: This is a simplified example
  # Real multi-layer effects would require engine support
  return base_anim
end

animation.register_user_function("rainbow_sparkle", rainbow_sparkle)

Dynamic Palettes~

Since DSL palettes only accept hex colors and predefined color names (not custom colors), use user functions for dynamic palettes with custom colors:

def create_custom_palette(engine, base_color, variation_count, intensity)
  # Create a palette with variations of the base color
  var palette_bytes = bytes()

  # Extract RGB components from base color
  var r = (base_color >> 16) & 0xFF
  var g = (base_color >> 8) & 0xFF
  var b = base_color & 0xFF

  # Create palette entries with color variations
  for i : 0..(variation_count-1)
    var position = int(i * 255 / (variation_count - 1))
    var factor = intensity * i / (variation_count - 1) / 255

    var new_r = int(r * factor)
    var new_g = int(g * factor)
    var new_b = int(b * factor)

    # Add VRGB entry (Value, Red, Green, Blue)
    palette_bytes.add(position, 1)  # Position
    palette_bytes.add(new_r, 1)     # Red
    palette_bytes.add(new_g, 1)     # Green  
    palette_bytes.add(new_b, 1)     # Blue
  end

  return palette_bytes
end

animation.register_user_function("custom_palette", create_custom_palette)
# Use dynamic colors in DSL
animation gradient_effect = rich_palette(
  colors=custom_palette(0xFF6B35, 5, 255)
  period=4s
)

run gradient_effect

Preset Configurations~

def police_lights(engine, flash_speed)
  var anim = animation.pulsating_animation(engine)
  anim.color = 0xFFFF0000  # Red
  anim.min_brightness = 0
  anim.max_brightness = 255
  anim.period = flash_speed
  return anim
end

def warning_strobe(engine)
  return police_lights(engine, 200)  # Fast strobe
end

def gentle_alert(engine)
  return police_lights(engine, 1000)  # Slow pulse
end

animation.register_user_function("police", police_lights)
animation.register_user_function("strobe", warning_strobe)
animation.register_user_function("alert", gentle_alert)
animation emergency = solid(color=red)
emergency.opacity = strobe()

animation notification = solid(color=yellow)
notification.opacity = alert()

animation custom_police = solid(color=blue)
custom_police.opacity = police(500)

Function Organization~

Single File Approach~

# user_animations.be
import animation

def breathing(engine, color, period)
  # ... implementation
end

def fire_effect(engine, intensity, speed)
  # ... implementation  
end

def twinkle_effect(engine, color, count, period)
  # ... implementation
end

# Register all functions
animation.register_user_function("breathing", breathing)
animation.register_user_function("fire", fire_effect)
animation.register_user_function("twinkle", twinkle_effect)

print("Custom animations loaded!")

Modular Approach~

# animations/fire.be
def fire_effect(engine, intensity, speed)
  # ... implementation
end

def torch_effect(engine)
  return fire_effect(engine, 255, 500)
end

return {
  'fire': fire_effect,
  'torch': torch_effect
}
# main.be
import animation

# Register functions
animation.register_user_function("fire", fire_effects['fire'])
animation.register_user_function("torch", fire_effects['torch'])

Best Practices~

Function Design~

  1. Use descriptive names: breathing_slow not bs
  2. Logical parameter order: color first, then timing, then modifiers
  3. Sensible defaults: Make functions work with minimal parameters
  4. Return animations: Always return a configured animation object

Parameter Handling~

def flexible_pulse(engine, color, period, min_brightness, max_brightness)
  # Provide defaults for optional parameters
  if min_brightness == nil min_brightness = 50 end
  if max_brightness == nil max_brightness = 255 end

  var anim = animation.pulsating_animation(engine)
  anim.color = color
  anim.period = period
  anim.min_brightness = min_brightness
  anim.max_brightness = max_brightness
  return anim
end

Error Handling~

def safe_comet(engine, color, tail_length, speed)
  # Validate parameters
  if tail_length < 1 tail_length = 1 end
  if tail_length > 20 tail_length = 20 end
  if speed < 100 speed = 100 end

  var anim = animation.comet_animation(engine)
  anim.color = color
  anim.tail_length = tail_length
  anim.speed = speed
  return anim
end

Documentation~

# Creates a pulsing animation with customizable brightness range
# Parameters:
#   color: The color to pulse (hex or named color)
#   period: How long one pulse cycle takes (in milliseconds)
#   min_brightness: Minimum brightness (0-255, default: 50)
#   max_brightness: Maximum brightness (0-255, default: 255)
# Returns: Configured pulse animation
def breathing_effect(engine, color, period, min_brightness, max_brightness)
  # ... implementation
end

User Functions in Computed Parameters~

User functions can be used in computed parameter expressions alongside mathematical functions, creating powerful dynamic animations:

Simple User Function in Computed Parameter~

# Simple user function call in property assignment
animation base = solid(color=blue, priority=10)
base.opacity = rand_demo()  # User function as computed parameter

User Functions with Mathematical Operations~

# Get strip length for calculations
set strip_len = strip_length()

# Mix user functions with mathematical functions
animation dynamic_solid = solid(
  color=purple
  opacity=max(50, min(255, rand_demo() + 100))  # Random opacity with bounds
  priority=15
)

User Functions in Complex Expressions~

# Use user function in arithmetic expressions
animation random_effect = solid(
  color=cyan
  opacity=abs(rand_demo() - 128) + 64  # Random variation around middle value
  priority=12
)

How It Works~

When you use user functions in computed parameters:

  1. Automatic Detection: The transpiler automatically detects user functions in expressions
  2. Single Closure: The entire expression is wrapped in a single efficient closure
  3. Engine Access: User functions receive engine in the closure context
  4. Mixed Operations: User functions work seamlessly with mathematical functions and arithmetic

Generated Code Example:

# DSL code
animation.opacity = max(100, breathing(red, 2000))

Transpiles to:

animation.opacity = animation.create_closure_value(engine, 
  def (engine, param_name, time_ms) 
    return (animation._math.max(100, animation.get_user_function('breathing')(engine, 0xFFFF0000, 2000))) 
  end)

Available User Functions~

The following user functions are available by default:

Function Parameters Description
rand_demo() none Returns a random value (0-255) for demonstration

Best Practices for Computed Parameters~

  1. Keep expressions readable: Break complex expressions across multiple lines
  2. Use meaningful variable names: set strip_len = strip_length() not set s = strip_length()
  3. Combine wisely: Mix user functions with math functions for rich effects
  4. Test incrementally: Start simple and build up complex expressions

Loading and Using Functions~

In Tasmota autoexec.be~

import animation

# Load your custom functions
load("user_animations.be")

# Now they're available in DSL with import
var dsl_code = 
  "import user_functions\n"
  "\n"
  "animation my_fire = solid(color=red)\n"
  "my_fire.opacity = fire(200, 1500)\n"
  "animation my_twinkles = solid(color=white)\n"
  "my_twinkles.opacity = twinkle(8, 400ms)\n"
  "\n"
  "sequence show {\n"
  "  play my_fire for 10s\n"
  "  play my_twinkles for 5s\n"
  "}\n"
  "\n"
  "run show"

animation_dsl.execute(dsl_code)

From Files~

# Save DSL with custom functions
var my_show =
  "import user_functions\n"
  "\n"
  "animation campfire = solid(color=orange)\n"
  "campfire.opacity = fire(180, 2000)\n"
  "animation stars = solid(color=0xFFFFFF)\n"
  "stars.opacity = twinkle(6, 600ms)\n"
  "\n"
  "sequence night_scene {\n"
  "  play campfire for 30s\n"
  "  play stars for 10s\n"
  "}\n"
  "\n"
  "run night_scene"

# Save to file
var f = open("night_scene.anim", "w")
f.write(my_show)
f.close()

# Load and run
animation_dsl.load_file("night_scene.anim")

Implementation Details~

Function Signature Requirements~

User functions must follow this exact pattern:

def function_name(engine, param1, param2, ...)
  # engine is ALWAYS the first parameter
  # followed by user-provided parameters
  return animation_object
end

How the DSL Transpiler Works~

When you write DSL like this:

animation my_anim = my_function(arg1, arg2)

The transpiler generates Berry code like this:

var my_anim_ = animation.get_user_function('my_function')(engine, arg1, arg2)

The engine parameter is automatically inserted as the first argument.

Registration API~

# Register a function
animation.register_user_function(name, function)

# Check if a function is registered
if animation.is_user_function("my_function")
  print("Function is registered")
end

# Get a registered function
var func = animation.get_user_function("my_function")

# List all registered functions
var functions = animation.list_user_functions()
for name : functions
  print("Registered:", name)
end

Engine Parameter~

The engine parameter provides: - Access to the LED strip: engine.get_strip_length() - Current time: engine.time_ms - Animation management context

Always use the provided engine when creating animations - don't create your own engine instances.

Return Value Requirements~

User functions must return an animation object that: - Extends animation.animation or animation.pattern - Is properly configured with the engine - Has all required parameters set

Error Handling~

The framework handles errors gracefully: - Invalid function names are caught at DSL compile time - Runtime errors in user functions are reported with context - Failed function calls don't crash the animation system

Troubleshooting~

Function Not Found~

Error: Unknown function 'my_function'
- Ensure the function is registered with animation.register_user_function() - Check that registration happens before DSL compilation - Verify the function name matches exactly (case-sensitive)

Wrong Number of Arguments~

Error: Function call failed
- Check that your function signature matches the DSL call - Remember that engine is automatically added as the first parameter - Verify all required parameters are provided in the DSL

Animation Not Working~

  • Ensure your function returns a valid animation object
  • Check that the animation is properly configured
  • Verify that the engine parameter is used correctly

User-defined functions provide a powerful way to extend the Animation DSL with custom effects while maintaining the clean, declarative syntax that makes the DSL easy to use.