16 KiB
Animation Development Guide
Guide for developers creating custom animation classes in the Berry Animation Framework.
Overview
Note: This guide is for developers who want to extend the framework by creating new animation classes. For using existing animations, see the DSL Reference which provides a declarative way to create animations without programming.
The Berry Animation Framework uses a unified architecture where all visual elements inherit from the base Animation class. This guide explains how to create custom animation classes that integrate seamlessly with the framework's parameter system, value providers, and rendering pipeline.
Animation Class Structure
Basic Class Template
#@ solidify:MyAnimation,weak
class MyAnimation : animation.animation
# NO instance variables for parameters - they are handled by the virtual parameter system
# Parameter definitions following the new specification
static var PARAMS = {
"my_param1": {"default": "default_value", "type": "string"},
"my_param2": {"min": 0, "max": 255, "default": 100, "type": "int"}
# Do NOT include inherited Animation parameters here
}
def init(engine)
# Engine parameter is MANDATORY and cannot be nil
super(self).init(engine)
# Only initialize non-parameter instance variables (none in this example)
# Parameters are handled by the virtual parameter system
end
# Handle parameter changes (optional)
def on_param_changed(name, value)
# Add custom logic for parameter changes if needed
# Parameter validation is handled automatically by the framework
end
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Use virtual parameter access - automatically resolves ValueProviders
var param1 = self.my_param1
var param2 = self.my_param2
# Your rendering logic here
# ...
return true
end
# NO setter methods needed - use direct virtual parameter assignment:
# obj.my_param1 = value
# obj.my_param2 = value
def tostring()
return f"MyAnimation(param1={self.my_param1}, param2={self.my_param2}, running={self.is_running})"
end
end
PARAMS System
Static Parameter Definition
The PARAMS static variable defines all parameters specific to your animation class. This system provides:
- Parameter validation with min/max constraints and type checking
- Default value handling for initialization
- Virtual parameter access through getmember/setmember
- Automatic ValueProvider resolution
Parameter Definition Format
static var PARAMS = {
"parameter_name": {
"default": default_value, # Default value (optional)
"min": minimum_value, # Minimum value for integers (optional)
"max": maximum_value, # Maximum value for integers (optional)
"enum": [val1, val2, val3], # Valid enum values (optional)
"type": "parameter_type", # Expected type (optional)
"nillable": true # Whether nil values are allowed (optional)
}
}
Supported Types
"int"- Integer values (default if not specified)"string"- String values"bool"- Boolean values (true/false)"bytes"- Bytes objects (validated using isinstance())"instance"- Object instances"any"- Any type (no type validation)
Important Rules
- Do NOT include inherited parameters - Animation base class parameters are handled automatically
- Only define class-specific parameters in your PARAMS
- No constructor parameter mapping - the new system uses engine-only constructors
- Parameters are accessed via virtual members:
obj.param_name
Constructor Implementation
Engine-Only Constructor Pattern
def init(engine)
# 1. ALWAYS call super with engine (engine is the ONLY parameter)
super(self).init(engine)
# 2. Initialize non-parameter instance variables only
self.internal_state = initial_value
self.buffer = nil
# Do NOT initialize parameters here - they are handled by the virtual system
end
Parameter Change Handling
def on_param_changed(name, value)
# Optional method to handle parameter changes
if name == "scale"
# Recalculate internal state when scale changes
self._update_internal_buffers()
elif name == "color"
# Handle color changes
self._invalidate_color_cache()
end
end
Key Changes from Old System
- Engine-only constructor: Constructor takes ONLY the engine parameter
- No parameter initialization: Parameters are set by caller using virtual member assignment
- No instance variables for parameters: Parameters are handled by the virtual system
- Automatic validation: Parameter validation happens automatically based on PARAMS constraints
Value Provider Integration
Automatic ValueProvider Resolution
The virtual parameter system automatically resolves ValueProviders when you access parameters:
def render(frame, time_ms)
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Virtual parameter access automatically resolves ValueProviders
var color = self.color # Returns current color value, not the provider
var position = self.pos # Returns current position value
var size = self.size # Returns current size value
# Use resolved values in rendering logic
for i: position..(position + size - 1)
if i >= 0 && i < frame.width
frame.set_pixel_color(i, color)
end
end
return true
end
Setting Dynamic Parameters
Users can set both static values and ValueProviders using the same syntax:
# Create animation
var anim = animation.my_animation(engine)
# Static values
anim.color = 0xFFFF0000
anim.pos = 5
anim.size = 3
# Dynamic values
anim.color = animation.smooth(0xFF000000, 0xFFFFFFFF, 2000)
anim.pos = animation.triangle(0, 29, 3000)
Performance Optimization
For performance-critical code, cache parameter values:
def render(frame, time_ms)
# Cache parameter values to avoid multiple virtual member access
var current_color = self.color
var current_pos = self.pos
var current_size = self.size
# Use cached values in loops
for i: current_pos..(current_pos + current_size - 1)
if i >= 0 && i < frame.width
frame.set_pixel_color(i, current_color)
end
end
return true
end
Parameter Access
Direct Virtual Member Assignment
The new system uses direct parameter assignment instead of setter methods:
# Create animation
var anim = animation.my_animation(engine)
# Direct parameter assignment (recommended)
anim.color = 0xFF00FF00
anim.pos = 10
anim.size = 5
# Method chaining is not needed - just set parameters directly
Parameter Validation
The parameter system handles validation automatically based on PARAMS constraints:
# This will raise an exception due to min: 0 constraint
anim.size = -1 # Raises value_error
# This will be accepted
anim.size = 5 # Parameter updated successfully
# Method-based setting returns true/false for validation
var success = anim.set_param("size", -1) # Returns false, no exception
Accessing Raw Parameters
# Get current parameter value (resolved if ValueProvider)
var current_color = anim.color
# Get raw parameter (returns ValueProvider if set)
var raw_color = anim.get_param("color")
# Check if parameter is a ValueProvider
if animation.is_value_provider(raw_color)
print("Color is dynamic")
else
print("Color is static")
end
Rendering Implementation
Frame Buffer Operations
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Get frame dimensions
var width = frame.width
var height = frame.height # Usually 1 for LED strips
# Resolve dynamic parameters
var color = self.resolve_value(self.color, "color", time_ms)
var opacity = self.resolve_value(self.opacity, "opacity", time_ms)
# Render your effect
for i: 0..(width-1)
var pixel_color = calculate_pixel_color(i, time_ms)
frame.set_pixel_color(i, pixel_color)
end
# Apply opacity if not full (supports numbers, animations)
if opacity < 255
frame.apply_opacity(opacity)
end
return true # Frame was modified
end
Common Rendering Patterns
Fill Pattern
# Fill entire frame with color
frame.fill_pixels(color)
Position-Based Effects
# Render at specific positions
var start_pos = self.resolve_value(self.pos, "pos", time_ms)
var size = self.resolve_value(self.size, "size", time_ms)
for i: 0..(size-1)
var pixel_pos = start_pos + i
if pixel_pos >= 0 && pixel_pos < frame.width
frame.set_pixel_color(pixel_pos, color)
end
end
Gradient Effects
# Create gradient across frame
for i: 0..(frame.width-1)
var progress = i / (frame.width - 1.0) # 0.0 to 1.0
var interpolated_color = interpolate_color(start_color, end_color, progress)
frame.set_pixel_color(i, interpolated_color)
end
Complete Example: BeaconAnimation
Here's a complete example showing all concepts:
#@ solidify:BeaconAnimation,weak
class BeaconAnimation : animation.animation
# NO instance variables for parameters - they are handled by the virtual parameter system
# Parameter definitions following the new specification
static var PARAMS = {
"color": {"default": 0xFFFFFFFF},
"back_color": {"default": 0xFF000000},
"pos": {"default": 0},
"beacon_size": {"min": 0, "default": 1},
"slew_size": {"min": 0, "default": 0}
}
# Initialize a new Pulse Position animation
# Engine parameter is MANDATORY and cannot be nil
def init(engine)
# Call parent constructor with engine (engine is the ONLY parameter)
super(self).init(engine)
# Only initialize non-parameter instance variables (none in this case)
# Parameters are handled by the virtual parameter system
end
# Handle parameter changes (optional - can be removed if no special handling needed)
def on_param_changed(name, value)
# No special handling needed for this animation
# Parameter validation is handled automatically by the framework
end
# Render the pulse to the provided frame buffer
def render(frame, time_ms)
if frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
end
var pixel_size = frame.width
# Use virtual parameter access - automatically resolves ValueProviders
var back_color = self.back_color
var pos = self.pos
var slew_size = self.slew_size
var beacon_size = self.beacon_size
var color = self.color
# Fill background if not transparent
if back_color != 0xFF000000
frame.fill_pixels(back_color)
end
# Calculate pulse boundaries
var pulse_min = pos
var pulse_max = pos + beacon_size
# Clamp to frame boundaries
if pulse_min < 0
pulse_min = 0
end
if pulse_max >= pixel_size
pulse_max = pixel_size
end
# Draw the main pulse
var i = pulse_min
while i < pulse_max
frame.set_pixel_color(i, color)
i += 1
end
# Draw slew regions if slew_size > 0
if slew_size > 0
# Left slew (fade from background to pulse color)
var left_slew_min = pos - slew_size
var left_slew_max = pos
if left_slew_min < 0
left_slew_min = 0
end
if left_slew_max >= pixel_size
left_slew_max = pixel_size
end
i = left_slew_min
while i < left_slew_max
# Calculate blend factor
var blend_factor = tasmota.scale_uint(i, pos - slew_size, pos - 1, 255, 0)
var alpha = 255 - blend_factor
var blend_color = (alpha << 24) | (color & 0x00FFFFFF)
var blended_color = frame.blend(back_color, blend_color)
frame.set_pixel_color(i, blended_color)
i += 1
end
# Right slew (fade from pulse color to background)
var right_slew_min = pos + beacon_size
var right_slew_max = pos + beacon_size + slew_size
if right_slew_min < 0
right_slew_min = 0
end
if right_slew_max >= pixel_size
right_slew_max = pixel_size
end
i = right_slew_min
while i < right_slew_max
# Calculate blend factor
var blend_factor = tasmota.scale_uint(i, pos + beacon_size, pos + beacon_size + slew_size - 1, 0, 255)
var alpha = 255 - blend_factor
var blend_color = (alpha << 24) | (color & 0x00FFFFFF)
var blended_color = frame.blend(back_color, blend_color)
frame.set_pixel_color(i, blended_color)
i += 1
end
end
return true
end
# NO setter methods - use direct virtual parameter assignment instead:
# obj.color = value
# obj.pos = value
# obj.beacon_size = value
# obj.slew_size = value
# String representation of the animation
def tostring()
return f"BeaconAnimation(color=0x{self.color :08x}, pos={self.pos}, beacon_size={self.beacon_size}, slew_size={self.slew_size})"
end
end
# Export class directly - no redundant factory function needed
return {'beacon_animation': BeaconAnimation}
Testing Your Animation
Unit Tests
Create comprehensive tests for your animation:
import animation
def test_my_animation()
# Create LED strip and engine for testing
var strip = global.Leds(10) # Use built-in LED strip for testing
var engine = animation.animation_engine(strip)
# Test basic construction
var anim = animation.my_animation(engine)
assert(anim != nil, "Animation should be created")
# Test parameter setting
anim.color = 0xFFFF0000
assert(anim.color == 0xFFFF0000, "Color should be set")
# Test parameter updates
anim.color = 0xFF00FF00
assert(anim.color == 0xFF00FF00, "Color should be updated")
# Test value providers
var dynamic_color = animation.smooth(engine)
dynamic_color.min_value = 0xFF000000
dynamic_color.max_value = 0xFFFFFFFF
dynamic_color.duration = 2000
anim.color = dynamic_color
var raw_color = anim.get_param("color")
assert(animation.is_value_provider(raw_color), "Should accept value provider")
# Test rendering
var frame = animation.frame_buffer(10)
anim.start()
var result = anim.render(frame, 1000)
assert(result == true, "Should render successfully")
print("✓ All tests passed")
end
test_my_animation()
Integration Testing
Test with the animation engine:
var strip = global.Leds(30) # Use built-in LED strip
var engine = animation.animation_engine(strip)
var anim = animation.my_animation(engine)
# Set parameters
anim.color = 0xFFFF0000
anim.pos = 5
anim.beacon_size = 3
engine.add(anim) # Unified method for animations and sequence managers
engine.run()
# Let it run for a few seconds
tasmota.delay(3000)
engine.stop()
print("Integration test completed")
Best Practices
Performance
- Minimize calculations in render() method
- Cache resolved values when possible
- Use integer math instead of floating point
- Avoid memory allocation in render loops
Memory Management
- Reuse objects when possible
- Clear references to large objects when done
- Use static variables for constants
Code Organization
- Group related parameters together
- Use descriptive variable names
- Comment complex algorithms
- Follow Berry naming conventions
Error Handling
- Validate parameters in constructor
- Handle edge cases gracefully
- Return false from render() on errors
- Use meaningful error messages
Publishing Your Animation Class
Once you've created a new animation class:
- Add it to the animation module by importing it in
animation.be - Create a factory function following the engine-first pattern
- Add DSL support by ensuring the transpiler recognizes your factory function
- Document parameters in the class hierarchy documentation
- Test with DSL to ensure users can access your animation declaratively
Remember: Users should primarily interact with animations through the DSL. The programmatic API is mainly for framework development and advanced integrations.
This guide provides everything needed to create professional-quality animation classes that integrate seamlessly with the Berry Animation Framework's parameter system and rendering pipeline.