Files
Tasmota/lib/libesp32/berry_animation/docs/USER_FUNCTIONS.md
2025-09-29 09:49:41 +02:00

17 KiB

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.palette = animation.PALETTE_FIRE
  color_provider.cycle_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.palette = animation.PALETTE_RAINBOW
  rainbow_provider.cycle_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 palette in DSL
animation gradient_effect = rich_palette(
  palette=custom_palette(0xFF6B35, 5, 255)
  cycle_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.