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_functionstranspiles to Berryimport "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
- Use descriptive names:
breathing_slownotbs - Logical parameter order: color first, then timing, then modifiers
- Sensible defaults: Make functions work with minimal parameters
- 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:
- Automatic Detection: The transpiler automatically detects user functions in expressions
- Single Closure: The entire expression is wrapped in a single efficient closure
- Engine Access: User functions receive
enginein the closure context - 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
- Keep expressions readable: Break complex expressions across multiple lines
- Use meaningful variable names:
set strip_len = strip_length()notset s = strip_length() - Combine wisely: Mix user functions with math functions for rich effects
- 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.animationoranimation.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
engineis 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.