# 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: ```berry # 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: ```berry 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: ```berry # 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: ```berry # 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`: ```berry 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 ```berry 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: ```berry 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 ```berry 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) ``` ```berry animation bright_red = solid(color=red) bright_red.opacity = bright(80) animation dim_blue = solid(color=blue) dim_blue.opacity = bright(30) ``` ### Fire Effects ```berry 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) ``` ```berry animation campfire = solid(color=red) campfire.opacity = fire(200, 2000) animation torch = solid(color=orange) torch.opacity = fire(255, 500) ``` ### Twinkling Effects ```berry 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) ``` ```berry 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 ```berry 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) ``` ```berry 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 ```berry 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: ```berry 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) ``` ```berry # Use dynamic palette in DSL animation gradient_effect = rich_palette( palette=custom_palette(0xFF6B35, 5, 255) cycle_period=4s ) run gradient_effect ``` ### Preset Configurations ```berry 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) ``` ```berry 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 ```berry # 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 ```berry # 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 } ``` ```berry # 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 ```berry 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 ```berry 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 ```berry # 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 ```berry # 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 ```berry # 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 ```berry # 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:** ```berry # DSL code animation.opacity = max(100, breathing(red, 2000)) ``` **Transpiles to:** ```berry 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 ```berry 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 ```berry # 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: ```berry 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: ```berry animation my_anim = my_function(arg1, arg2) ``` The transpiler generates Berry code like this: ```berry var my_anim_ = animation.get_user_function('my_function')(engine, arg1, arg2) ``` The `engine` parameter is automatically inserted as the first argument. ### Registration API ```berry # 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.