Berry animation template (#23851)

This commit is contained in:
s-hadinger 2025-08-29 23:10:41 +02:00 committed by GitHub
parent 5a08bc11e3
commit d5114c32c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 11190 additions and 8924 deletions

View File

@ -63,6 +63,32 @@ animation sunset_glow = rich_palette(
run sunset_glow
```
### Reusable Templates
Create parameterized animation patterns that can be reused with different settings:
```berry
# Define a reusable template
template pulse_effect {
param color type color
param speed
param brightness
animation pulse = pulsating_animation(
color=color
period=speed
opacity=brightness
)
run pulse
}
# Use the template with different parameters
pulse_effect(red, 2s, 255) # Bright red pulse
pulse_effect(blue, 1s, 150) # Dimmer blue pulse
pulse_effect(0xFF69B4, 3s, 200) # Hot pink pulse
```
### Animation Sequences
```berry

View File

@ -0,0 +1,64 @@
# Generated Berry code from Animation DSL
# Source: cylon_generic.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Cylon Red Eye
# Automatically adapts to the length of the strip
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: cylon_effect
def cylon_effect_template(engine, eye_color_, back_color_, duration_)
var strip_len_ = animation.strip_length(engine)
var eye_animation_ = animation.beacon_animation(engine)
eye_animation_.color = eye_color_
eye_animation_.back_color = back_color_
eye_animation_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.duration = duration_
return provider
end)(engine)
eye_animation_.beacon_size = 3 # small 3 pixels eye
eye_animation_.slew_size = 2 # with 2 pixel shading around
eye_animation_.priority = 5
engine.add_animation(eye_animation_)
end
animation.register_user_function('cylon_effect', cylon_effect_template)
cylon_effect_template(engine, 0xFFFF0000, 0x00000000, 3000)
engine.start()
#- Original DSL source:
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
param eye_color type color
param back_color type color
param duration
set strip_len = strip_length()
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5
)
run eye_animation
}
cylon_effect(red, transparent, 3s)
-#

View File

@ -0,0 +1,62 @@
# Generated Berry code from Animation DSL
# Source: import_demo.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Import Demo - Demonstrates DSL import functionality
# This example shows how to import user functions and use them in animations
# Import user functions module
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
import user_functions
# Create animations that use imported user functions
var random_red_ = animation.solid(engine)
random_red_.color = 0xFFFF0000
random_red_.opacity = animation.create_closure_value(engine, def (self) return animation.get_user_function('rand_demo')(self.engine) end)
var breathing_blue_ = animation.solid(engine)
breathing_blue_.color = 0xFF0000FF
breathing_blue_.opacity = animation.create_closure_value(engine, def (self) return self.max(50, self.min(255, animation.get_user_function('rand_demo')(self.engine) + 100)) end)
var dynamic_green_ = animation.solid(engine)
dynamic_green_.color = 0xFF008000
dynamic_green_.opacity = animation.create_closure_value(engine, def (self) return self.abs(animation.get_user_function('rand_demo')(self.engine) - 128) + 64 end)
# Create a sequence that cycles through the animations
var import_demo_ = animation.SequenceManager(engine)
.push_play_step(random_red_, 3000)
.push_play_step(breathing_blue_, 3000)
.push_play_step(dynamic_green_, 3000)
# Run the demo
engine.add_sequence_manager(import_demo_)
engine.start()
#- Original DSL source:
# Import Demo - Demonstrates DSL import functionality
# This example shows how to import user functions and use them in animations
# Import user functions module
import user_functions
# Create animations that use imported user functions
animation random_red = solid(color=red)
random_red.opacity = user.rand_demo()
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = max(50, min(255, user.rand_demo() + 100))
animation dynamic_green = solid(color=green)
dynamic_green.opacity = abs(user.rand_demo() - 128) + 64
# Create a sequence that cycles through the animations
sequence import_demo {
play random_red for 3s
play breathing_blue for 3s
play dynamic_green for 3s
}
# Run the demo
run import_demo
-#

View File

@ -0,0 +1,41 @@
# Generated Berry code from Animation DSL
# Source: pattern_fire.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Pattern fire.anim
# Define fire palette from black to yellow
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var fire_colors_ = bytes("FF800000" "FFFF0000" "FFFF4500" "FFFFFF00")
var strip_len_ = animation.strip_length(engine)
var fire_color_ = animation.rich_palette(engine)
fire_color_.palette = fire_colors_
var fire_pattern_ = animation.palette_gradient_animation(engine)
fire_pattern_.color_source = fire_color_
fire_pattern_.spatial_period = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) / 2 end)
engine.add_animation(fire_pattern_)
engine.start()
#- Original DSL source:
# Pattern fire.anim
# Define fire palette from black to yellow
palette fire_colors = [
0x800000 # Dark red
0xFF0000 # Red
0xFF4500 # Orange red
0xFFFF00 # Yellow
]
set strip_len = strip_length()
color fire_color = rich_palette(palette=fire_colors)
animation fire_pattern = palette_gradient_animation(color_source=fire_color, spatial_period=strip_len/2)
run fire_pattern
-#

View File

@ -0,0 +1,88 @@
# Generated Berry code from Animation DSL
# Source: test_complex_template.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Complex template test
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: rainbow_pulse
def rainbow_pulse_template(engine, pal1_, pal2_, duration_, back_color_)
var cycle_color_ = animation.color_cycle(engine)
cycle_color_.palette = pal1_
cycle_color_.cycle_period = duration_
# Create pulsing animation
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = cycle_color_
pulse_.period = duration_
# Create background
var background_ = animation.solid(engine)
background_.color = back_color_
background_.priority = 1
# Set pulse priority higher
pulse_.priority = 10
# Run both animations
engine.add_animation(background_)
engine.add_animation(pulse_)
end
animation.register_user_function('rainbow_pulse', rainbow_pulse_template)
# Create palettes
var fire_palette_ = bytes("00000000" "80FF0000" "FFFFFF00")
var ocean_palette_ = bytes("00000080" "800080FF" "FF00FFFF")
# Use the template
rainbow_pulse_template(engine, fire_palette_, ocean_palette_, 3000, 0xFF001100)
engine.start()
#- Original DSL source:
# Complex template test
template rainbow_pulse {
param pal1 type palette
param pal2 type palette
param duration
param back_color type color
# Create color cycle using first palette
color cycle_color = color_cycle(palette=pal1, cycle_period=duration)
# Create pulsing animation
animation pulse = pulsating_animation(
color=cycle_color
period=duration
)
# Create background
animation background = solid(color=back_color)
background.priority = 1
# Set pulse priority higher
pulse.priority = 10
# Run both animations
run background
run pulse
}
# Create palettes
palette fire_palette = [
(0, 0x000000)
(128, 0xFF0000)
(255, 0xFFFF00)
]
palette ocean_palette = [
(0, 0x000080)
(128, 0x0080FF)
(255, 0x00FFFF)
]
# Use the template
rainbow_pulse(fire_palette, ocean_palette, 3s, 0x001100)
-#

View File

@ -0,0 +1,50 @@
# Generated Berry code from Animation DSL
# Source: test_template_simple.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Test template functionality
# Define a simple template
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: pulse_effect
def pulse_effect_template(engine, base_color_, duration_, brightness_)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add_animation(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)
# Use the template - templates add animations directly to engine and run them
pulse_effect_template(engine, 0xFFFF0000, 2000, 204)
engine.start()
#- Original DSL source:
# Test template functionality
# Define a simple template
template pulse_effect {
param base_color type color
param duration
param brightness
animation pulse = pulsating_animation(
color=base_color
period=duration
)
pulse.opacity = brightness
run pulse
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)
-#

View File

@ -0,0 +1,50 @@
# Generated Berry code from Animation DSL
# Source: test_template_simple_reusable.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Test template functionality
# Define a simple template
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: pulse_effect
def pulse_effect_template(engine, base_color_, duration_, brightness_)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add_animation(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)
# Use the template - templates add animations directly to engine and run them
pulse_effect_template(engine, 0xFFFF0000, 2000, 204)
engine.start()
#- Original DSL source:
# Test template functionality
# Define a simple template
template pulse_effect {
param base_color type color
param duration
param brightness
animation pulse = pulsating_animation(
color=base_color
period=duration
)
pulse.opacity = brightness
run pulse
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)
-#

View File

@ -8,10 +8,11 @@ import animation
# User Functions Demo - Advanced Computed Parameters
# Shows how to use user functions in computed parameters via property assignments
# Get the current strip length for calculations
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
import user_functions
# Get the current strip length for calculations
var strip_len_ = animation.strip_length(engine)
# Example 1: Simple user function in computed parameter
var random_base_ = animation.solid(engine)
@ -56,6 +57,8 @@ engine.start()
# User Functions Demo - Advanced Computed Parameters
# Shows how to use user functions in computed parameters via property assignments
import user_functions
# Get the current strip length for calculations
set strip_len = strip_length()
@ -65,7 +68,7 @@ animation random_base = solid(
priority=10
)
# Use user function in property assignment
random_base.opacity = rand_demo()
random_base.opacity = user.rand_demo()
# Example 2: User function with mathematical operations
animation random_bounded = solid(
@ -73,7 +76,7 @@ animation random_bounded = solid(
priority=8
)
# User function with bounds using math functions
random_bounded.opacity = max(50, min(255, rand_demo() + 100))
random_bounded.opacity = max(50, min(255, user.rand_demo() + 100))
# Example 3: User function in arithmetic expressions
animation random_variation = solid(
@ -81,7 +84,7 @@ animation random_variation = solid(
priority=15
)
# Mix user function with arithmetic operations
random_variation.opacity = abs(rand_demo() - 128) + 64
random_variation.opacity = abs(user.rand_demo() - 128) + 64
# Example 4: User function affecting different properties
animation random_multi = solid(
@ -89,7 +92,7 @@ animation random_multi = solid(
priority=12
)
# Use user function for multiple properties
random_multi.opacity = max(100, rand_demo())
random_multi.opacity = max(100, user.rand_demo())
# Example 5: Complex expression with user function
animation random_complex = solid(
@ -97,7 +100,7 @@ animation random_complex = solid(
priority=20
)
# Complex expression with user function and math operations
random_complex.opacity = round((rand_demo() + 128) / 2 + abs(rand_demo() - 100))
random_complex.opacity = round((user.rand_demo() + 128) / 2 + abs(user.rand_demo() - 100))
# Run all animations to demonstrate the effects
run random_base

View File

@ -0,0 +1,23 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
param eye_color type color
param back_color type color
param duration
set strip_len = strip_length()
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5
)
run eye_animation
}
cylon_effect(red, transparent, 3s)

View File

@ -1,23 +0,0 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
set strip_len = strip_length()
animation red_eye = beacon_animation(
color = red
pos = cosine_osc(min_value = 0, max_value = strip_len - 2, duration = 5s)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 10
)
animation green_eye = beacon_animation(
color = green
pos = strip_len - red_eye.pos
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 15 # behind red eye
)
run red_eye
run green_eye

View File

@ -0,0 +1,25 @@
# Import Demo - Demonstrates DSL import functionality
# This example shows how to import user functions and use them in animations
# Import user functions module
import user_functions
# Create animations that use imported user functions
animation random_red = solid(color=red)
random_red.opacity = user.rand_demo()
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = max(50, min(255, user.rand_demo() + 100))
animation dynamic_green = solid(color=green)
dynamic_green.opacity = abs(user.rand_demo() - 128) + 64
# Create a sequence that cycles through the animations
sequence import_demo {
play random_red for 3s
play breathing_blue for 3s
play dynamic_green for 3s
}
# Run the demo
run import_demo

View File

@ -0,0 +1,15 @@
# Pattern fire.anim
# Define fire palette from black to yellow
palette fire_colors = [
0x800000 # Dark red
0xFF0000 # Red
0xFF4500 # Orange red
0xFFFF00 # Yellow
]
set strip_len = strip_length()
color fire_color = rich_palette(palette=fire_colors)
animation fire_pattern = palette_gradient_animation(color_source=fire_color, spatial_period=strip_len/2)
run fire_pattern

View File

@ -0,0 +1,44 @@
# Complex template test
template rainbow_pulse {
param pal1 type palette
param pal2 type palette
param duration
param back_color type color
# Create color cycle using first palette
color cycle_color = color_cycle(palette=pal1, cycle_period=duration)
# Create pulsing animation
animation pulse = pulsating_animation(
color=cycle_color
period=duration
)
# Create background
animation background = solid(color=back_color)
background.priority = 1
# Set pulse priority higher
pulse.priority = 10
# Run both animations
run background
run pulse
}
# Create palettes
palette fire_palette = [
(0, 0x000000)
(128, 0xFF0000)
(255, 0xFFFF00)
]
palette ocean_palette = [
(0, 0x000080)
(128, 0x0080FF)
(255, 0x00FFFF)
]
# Use the template
rainbow_pulse(fire_palette, ocean_palette, 3s, 0x001100)

View File

@ -0,0 +1,19 @@
# Test template functionality
# Define a simple template
template pulse_effect {
param base_color type color
param duration
param brightness
animation pulse = pulsating_animation(
color=base_color
period=duration
)
pulse.opacity = brightness
run pulse
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)

View File

@ -0,0 +1,19 @@
# Test template functionality
# Define a simple template
template pulse_effect {
param base_color type color
param duration
param brightness
animation pulse = pulsating_animation(
color=base_color
period=duration
)
pulse.opacity = brightness
run pulse
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)

View File

@ -1,6 +1,8 @@
# User Functions Demo - Advanced Computed Parameters
# Shows how to use user functions in computed parameters via property assignments
import user_functions
# Get the current strip length for calculations
set strip_len = strip_length()
@ -10,7 +12,7 @@ animation random_base = solid(
priority=10
)
# Use user function in property assignment
random_base.opacity = rand_demo()
random_base.opacity = user.rand_demo()
# Example 2: User function with mathematical operations
animation random_bounded = solid(
@ -18,7 +20,7 @@ animation random_bounded = solid(
priority=8
)
# User function with bounds using math functions
random_bounded.opacity = max(50, min(255, rand_demo() + 100))
random_bounded.opacity = max(50, min(255, user.rand_demo() + 100))
# Example 3: User function in arithmetic expressions
animation random_variation = solid(
@ -26,7 +28,7 @@ animation random_variation = solid(
priority=15
)
# Mix user function with arithmetic operations
random_variation.opacity = abs(rand_demo() - 128) + 64
random_variation.opacity = abs(user.rand_demo() - 128) + 64
# Example 4: User function affecting different properties
animation random_multi = solid(
@ -34,7 +36,7 @@ animation random_multi = solid(
priority=12
)
# Use user function for multiple properties
random_multi.opacity = max(100, rand_demo())
random_multi.opacity = max(100, user.rand_demo())
# Example 5: Complex expression with user function
animation random_complex = solid(
@ -42,7 +44,7 @@ animation random_complex = solid(
priority=20
)
# Complex expression with user function and math operations
random_complex.opacity = round((rand_demo() + 128) / 2 + abs(rand_demo() - 100))
random_complex.opacity = round((user.rand_demo() + 128) / 2 + abs(user.rand_demo() - 100))
# Run all animations to demonstrate the effects
run random_base

View File

@ -73,8 +73,8 @@ Unified base class for all visual elements. Inherits from `ParameterizedObject`.
| `is_running` | bool | false | - | Whether the animation is active |
| `priority` | int | 10 | 0-255 | Rendering priority (higher = on top) |
| `duration` | int | 0 | min: 0 | Animation duration in ms (0 = infinite) |
| `loop` | bool | true | - | Whether to loop when duration is reached |
| `opacity` | int | 255 | 0-255 | Animation opacity/brightness |
| `loop` | bool | false | - | Whether to loop when duration is reached |
| `opacity` | any | 255 | - | Animation opacity (number, FrameBuffer, or Animation) |
| `color` | int | 0xFFFFFFFF | - | Base color in ARGB format |
**Special Behavior**: Setting `is_running = true/false` starts/stops the animation.
@ -1213,14 +1213,20 @@ animation zoomed_sparkles = scale_animation(
### PalettePatternAnimation
Applies colors from a color provider to specific patterns. Inherits from `Animation`.
Applies colors from a color provider to specific patterns using an efficient bytes() buffer. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color_source` | instance | nil | - | Color provider for pattern mapping |
| `pattern_func` | function | nil | - | Function that generates pattern values |
| `pattern_func` | function | nil | - | Function that generates pattern values (0-255) for each pixel |
| *(inherits all Animation parameters)* | | | | |
**Implementation Details:**
- Uses `bytes()` buffer for efficient storage of per-pixel values
- Pattern function should return values in 0-255 range
- Color source receives values in 0-255 range via `get_color_for_value(value, time_ms)`
- Buffer automatically resizes when strip length changes
**Factory**: `animation.palette_pattern_animation(engine)`
### PaletteWaveAnimation
@ -1233,6 +1239,11 @@ Creates sine wave patterns with palette colors. Inherits from `PalettePatternAni
| `wave_length` | int | 10 | min: 1 | Wave length in pixels |
| *(inherits all PalettePatternAnimation parameters)* | | | | |
**Pattern Generation:**
- Generates sine wave values in 0-255 range using `tasmota.sine_int()`
- Wave position advances based on `wave_period` timing
- Each pixel's value calculated as: `sine_value = tasmota.scale_int(sine_int(angle), -4096, 4096, 0, 255)`
**Factory**: `animation.palette_wave_animation(engine)`
### PaletteGradientAnimation
@ -1241,9 +1252,22 @@ Creates shifting gradient patterns with palette colors. Inherits from `PalettePa
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `shift_period` | int | 10000 | min: 1 | Gradient shift period in ms |
| `shift_period` | int | 10000 | min: 0 | Time for one complete shift cycle in ms (0 = static gradient) |
| `spatial_period` | int | 0 | min: 0 | Spatial period in pixels (0 = full strip length) |
| `phase_shift` | int | 0 | 0-100 | Phase shift as percentage of spatial period |
| *(inherits all PalettePatternAnimation parameters)* | | | | |
**Pattern Generation:**
- Generates linear gradient values in 0-255 range across the specified spatial period
- **shift_period**: Controls temporal movement - how long it takes for the gradient to shift one full spatial period
- `0`: Static gradient (no movement)
- `> 0`: Moving gradient with specified period in milliseconds
- **spatial_period**: Controls spatial repetition - how many pixels before the gradient pattern repeats
- `0`: Gradient spans the full strip length (single gradient across entire strip)
- `> 0`: Gradient repeats every N pixels
- **phase_shift**: Shifts the gradient pattern spatially by a percentage of the spatial period
- Each pixel's value calculated as: `value = tasmota.scale_uint(spatial_position, 0, spatial_period-1, 0, 255)`
**Factory**: `animation.palette_gradient_animation(engine)`
### PaletteMeterAnimation
@ -1252,9 +1276,14 @@ Creates meter/bar patterns based on a value function. Inherits from `PalettePatt
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `value_func` | function | nil | - | Function that provides meter values |
| `value_func` | function | nil | - | Function that provides meter values (0-100 range) |
| *(inherits all PalettePatternAnimation parameters)* | | | | |
**Pattern Generation:**
- Value function returns percentage (0-100) representing meter level
- Pixels within meter range get value 255, others get value 0
- Meter position calculated as: `position = tasmota.scale_uint(value, 0, 100, 0, strip_length)`
**Factory**: `animation.palette_meter_animation(engine)`
## Motion Effects

View File

@ -291,7 +291,7 @@ def render(frame, time_ms)
frame.set_pixel_color(i, pixel_color)
end
# Apply opacity if not full
# Apply opacity if not full (supports numbers, animations)
if opacity < 255
frame.apply_opacity(opacity)
end

View File

@ -55,12 +55,16 @@ The following keywords are reserved and cannot be used as identifiers:
**Configuration Keywords:**
- `strip` - Strip configuration (temporarily disabled, reserved keyword)
- `set` - Variable assignment
- `import` - Import Berry modules
**Definition Keywords:**
- `color` - Color definition
- `palette` - Palette definition
- `animation` - Animation definition
- `sequence` - Sequence definition
- `template` - Template definition
- `param` - Template parameter declaration
- `type` - Parameter type annotation
**Control Flow Keywords:**
- `play` - Play animation in sequence
@ -207,6 +211,46 @@ set position_sweep = triangle(min_value=0, max_value=29, period=5s)
set strip_len = strip_length() # Get current strip length
```
### Import Statements
The `import` keyword imports Berry modules for use in animations:
```berry
import user_functions # Import user-defined functions
import my_custom_module # Import custom animation libraries
import math # Import standard Berry modules
import string # Import utility modules
```
**Import Behavior:**
- Module names should be valid identifiers (no quotes needed in DSL)
- Import statements are typically placed at the beginning of DSL files
- Transpiles to standard Berry `import "module_name"` statements
- Imported modules become available for the entire animation
**Common Use Cases:**
```berry
# Import user functions for computed parameters
import user_functions
animation dynamic = solid(color=blue)
dynamic.opacity = user.my_custom_function()
# Import custom animation libraries
import fire_effects
animation campfire = fire_effects.create_fire(intensity=200)
```
**Transpilation Example:**
```berry
# DSL Code
import user_functions
# Transpiles to Berry Code
import "user_functions"
```
## Color Definitions
The `color` keyword defines static colors or color providers:
@ -335,11 +379,15 @@ pulse_red.opacity = smooth(min_value=100, max_value=255, period=2s)
set strip_len = strip_length()
pulse_red.position = strip_len / 2 # Center position
pulse_red.opacity = strip_len * 4 # Scale with strip size
# Animation opacity (using another animation as opacity mask)
animation opacity_mask = pulsating_animation(period=2s)
pulse_red.opacity = opacity_mask # Dynamic opacity from animation
```
**Common Properties:**
- `priority` - Animation priority (higher numbers have precedence)
- `opacity` - Opacity level (0-255)
- `opacity` - Opacity level (number, value provider, or animation)
- `position` - Position on strip
- `speed` - Speed multiplier
- `phase` - Phase offset
@ -443,37 +491,37 @@ test.opacity = min(255, max(50, scale(sqrt(strip_len), 0, 16, 100, 255)))
When the DSL detects arithmetic expressions containing value providers, variable references, or mathematical functions, it automatically creates closure functions that capture the computation. These closures are called with `(self, param_name, time_ms)` parameters, allowing the computation to be re-evaluated dynamically as needed. Mathematical functions are automatically prefixed with `self.` in the closure context to access the ClosureValueProvider's mathematical methods.
**User Functions in Computed Parameters:**
User-defined functions can also be used in computed parameter expressions, providing powerful custom effects:
User-defined functions can also be used in computed parameter expressions, providing powerful custom effects. User functions must be called with the `user.` prefix:
```berry
# Simple user function in computed parameter
animation base = solid(color=blue)
base.opacity = rand_demo()
base.opacity = user.rand_demo()
# User functions mixed with math operations
animation dynamic = solid(
color=purple
opacity=max(50, min(255, rand_demo() + 100))
opacity=max(50, min(255, user.rand_demo() + 100))
)
```
### User Functions
User functions are custom Berry functions that can be called from computed parameters. They provide dynamic values that change over time.
User functions are custom Berry functions that can be called from computed parameters. They provide dynamic values that change over time. User functions must be called with the `user.` prefix.
**Available User Functions:**
- `rand_demo()` - Returns random values for demonstration purposes
- `user.rand_demo()` - Returns random values for demonstration purposes
**Usage in Computed Parameters:**
```berry
# Simple user function
animation.opacity = rand_demo()
animation.opacity = user.rand_demo()
# User function with math operations
animation.opacity = max(100, rand_demo())
animation.opacity = max(100, user.rand_demo())
# User function in arithmetic expressions
animation.opacity = abs(rand_demo() - 128) + 64
animation.opacity = abs(user.rand_demo() - 128) + 64
```
**Available User Functions:**
@ -481,7 +529,7 @@ The following user functions are available by default (see [User Functions Guide
| Function | Parameters | Description |
|----------|------------|-------------|
| `rand_demo()` | none | Returns a random value (0-255) for demonstration |
| `user.rand_demo()` | none | Returns a random value (0-255) for demonstration |
**User Function Behavior:**
- User functions are automatically detected by the transpiler
@ -668,6 +716,167 @@ sequence cylon_eye {
}
```
## Templates
Templates provide a powerful way to create reusable, parameterized animation patterns. They allow you to define animation blueprints that can be instantiated with different parameters, promoting code reuse and maintainability.
### Template Definition
Templates are defined using the `template` keyword followed by a parameter block and body:
```berry
template template_name {
param parameter1 type color
param parameter2
param parameter3 type number
# Template body with DSL statements
animation my_anim = some_animation(color=parameter1, period=parameter2)
my_anim.opacity = parameter3
run my_anim
}
```
### Template Parameters
Template parameters are declared using the `param` keyword with optional type annotations:
```berry
template pulse_effect {
param base_color type color # Parameter with type annotation
param duration # Parameter without type annotation
param brightness type number # Another typed parameter
# Use parameters in template body
animation pulse = pulsating_animation(
color=base_color
period=duration
)
pulse.opacity = brightness
run pulse
}
```
**Parameter Types:**
- `color` - Color values (hex, named colors, color providers)
- `palette` - Palette definitions
- `number` - Numeric values (integers, percentages, time values)
- `animation` - Animation instances
- Type annotations are optional but improve readability
### Template Body
The template body can contain any valid DSL statements:
**Supported Statements:**
- Color definitions
- Palette definitions
- Animation definitions
- Property assignments
- Run statements
- Variable assignments (set statements)
```berry
template rainbow_pulse {
param pal1 as palette
param pal2 as palette
param duration
param back_color as color
# Create dynamic color cycling
color cycle_color = color_cycle(
palette=pal1
cycle_period=duration
)
# Create animations
animation pulse = pulsating_animation(
color=cycle_color
period=duration
)
animation background = solid(color=back_color)
# Set properties
background.priority = 1
pulse.priority = 10
# Run both animations
run background
run pulse
}
```
### Template Usage
Templates are called like functions with positional arguments:
```berry
# Define the template
template blink_red {
param speed
animation blink = pulsating_animation(
color=red
period=speed
)
run blink
}
# Use the template
blink_red(1s) # Call with 1 second period
blink_red(500ms) # Call with 500ms period
```
**Complex Template Usage:**
```berry
# Create palettes for the template
palette fire_palette = [
(0, black)
(128, red)
(255, yellow)
]
palette ocean_palette = [
(0, navy)
(128, cyan)
(255, white)
]
# Use the complex template
rainbow_pulse(fire_palette, ocean_palette, 3s, black)
```
### Template Behavior
**Code Generation:**
Templates generate Berry functions that are registered as user functions:
```berry
# Template definition generates:
def pulse_effect_template(engine, base_color_, duration_, brightness_)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add_animation(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)
```
**Parameter Handling:**
- Parameters get `_` suffix in generated code to avoid naming conflicts
- Templates receive `engine` as the first parameter automatically
- Template calls are converted to function calls with `engine` as first argument
**Execution Model:**
- Templates don't return values - they add animations directly to the engine
- Multiple `run` statements in templates add multiple animations
- Templates can be called multiple times to create multiple instances
- `engine.start()` is automatically called when templates are used at the top level
## Execution Statements
Execute animations or sequences:
@ -936,22 +1145,26 @@ good.priority = 10 # OK: Valid parameter assignment
program = { statement } ;
statement = config_stmt
statement = import_stmt
| config_stmt
| definition
| property_assignment
| sequence
| template_def
| execution_stmt ;
(* Configuration *)
(* Import and Configuration *)
import_stmt = "import" identifier ;
config_stmt = variable_assignment ;
(* strip_config = "strip" "length" number ; -- TEMPORARILY DISABLED *)
variable_assignment = "set" identifier "=" expression ;
(* Definitions *)
definition = color_def | palette_def | animation_def ;
definition = color_def | palette_def | animation_def | template_def ;
color_def = "color" identifier "=" color_expression ;
palette_def = "palette" identifier "=" palette_array ;
animation_def = "animation" identifier "=" animation_expression ;
template_def = "template" identifier "{" template_body "}" ;
(* Property Assignments *)
property_assignment = identifier "." identifier "=" expression ;
@ -966,8 +1179,16 @@ wait_stmt = "wait" time_expression ;
repeat_stmt = "repeat" ( number "times" | "forever" ) "{" sequence_body "}" ;
sequence_assignment = identifier "." identifier "=" expression ;
(* Templates *)
template_def = "template" identifier "{" template_body "}" ;
template_body = { template_statement } ;
template_statement = param_decl | color_def | palette_def | animation_def | property_assignment | execution_stmt ;
param_decl = "param" identifier [ "type" identifier ] ;
(* Execution *)
execution_stmt = "run" identifier ;
execution_stmt = "run" identifier | template_call ;
template_call = identifier "(" [ argument_list ] ")" ;
argument_list = expression { "," expression } ;
(* Expressions *)
expression = logical_or_expr ;

View File

@ -93,16 +93,21 @@ The Animation DSL uses a declarative syntax with named parameters. All animation
### Key Syntax Features
- **Import statements**: `import module_name` for loading Berry modules
- **Named parameters**: All function calls use `name=value` syntax
- **Time units**: `2s`, `500ms`, `1m`, `1h`
- **Hex colors**: `#FF0000`, `#80FF0000` (ARGB)
- **Named colors**: `red`, `blue`, `white`, etc.
- **Comments**: `# This is a comment`
- **Property assignment**: `animation.property = value`
- **User functions**: `user.function_name()` for custom functions
### Basic Structure
```berry
# Import statements (optional, for user functions or custom modules)
import user_functions
# Optional strip configuration
strip length 60
@ -114,8 +119,9 @@ color blue = #0000FF
animation pulse_red = pulsating_animation(color=red, period=2s)
animation comet_blue = comet_animation(color=blue, tail_length=10, speed=1500)
# Property assignments
# Property assignments with user functions
pulse_red.priority = 10
pulse_red.opacity = user.breathing_effect()
comet_blue.direction = -1
# Execution
@ -180,8 +186,186 @@ my_animation.priority = 10
This intelligent resolution ensures optimal performance while maintaining clear separation between framework and user code.
## Import Statement Transpilation
The DSL supports importing Berry modules using the `import` keyword, which provides a clean way to load user functions and custom modules.
### Import Syntax
```berry
# DSL Import Syntax
import user_functions
import my_custom_module
import math
```
### Transpilation Behavior
Import statements are transpiled directly to Berry import statements with quoted module names:
```berry
# DSL Code
import user_functions
# Transpiles to Berry Code
import "user_functions"
```
### Import Processing
1. **Early Processing**: Import statements are processed early in transpilation
2. **Module Loading**: Imported modules are loaded using standard Berry import mechanism
3. **Function Registration**: User function modules should register functions using `animation.register_user_function()`
4. **No Validation**: The DSL doesn't validate module existence at compile time
### Example Import Workflow
**Step 1: Create User Functions Module (`user_functions.be`)**
```berry
import animation
def rand_demo(engine)
import math
return math.rand() % 256
end
# Register for DSL use
animation.register_user_function("rand_demo", rand_demo)
```
**Step 2: Use in DSL**
```berry
import user_functions
animation test = solid(color=blue)
test.opacity = user.rand_demo()
run test
```
**Step 3: Generated Berry Code**
```berry
import animation
var engine = animation.init_strip()
import "user_functions"
var test_ = animation.solid(engine)
test_.color = 0xFF0000FF
test_.opacity = animation.create_closure_value(engine,
def (self) return animation.get_user_function('rand_demo')(self.engine) end)
engine.add_animation(test_)
engine.start()
```
## Advanced DSL Features
### Templates
Templates provide a DSL-native way to create reusable animation patterns with parameters. Templates are transpiled into Berry functions and automatically registered for use.
#### Template Definition Transpilation
```berry
# DSL Template
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(
color=color
period=speed
)
run pulse
}
```
**Transpiles to:**
```berry
def pulse_effect(engine, color, speed)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = color
pulse_.period = speed
engine.add_animation(pulse_)
engine.start_animation(pulse_)
end
animation.register_user_function("pulse_effect", pulse_effect)
```
#### Template Transpilation Process
1. **Function Generation**: Template becomes a Berry function with `engine` as first parameter
2. **Parameter Mapping**: Template parameters become function parameters (after `engine`)
3. **Body Transpilation**: Template body is transpiled using standard DSL rules
4. **Auto-Registration**: Generated function is automatically registered as a user function
5. **Type Annotations**: Optional `type` annotations are preserved as comments for documentation
#### Template Call Transpilation
```berry
# DSL Template Call
pulse_effect(red, 2s)
```
**Transpiles to:**
```berry
pulse_effect(engine, animation.red, 2000)
```
Template calls are transpiled as regular user function calls with automatic `engine` parameter injection.
#### Advanced Template Features
**Multi-Animation Templates:**
```berry
template comet_chase {
param trail_color type color
param bg_color type color
param chase_speed
animation background = solid_animation(color=bg_color)
animation comet = comet_animation(color=trail_color, speed=chase_speed)
run background
run comet
}
```
**Transpiles to:**
```berry
def comet_chase(engine, trail_color, bg_color, chase_speed)
var background_ = animation.solid_animation(engine)
background_.color = bg_color
var comet_ = animation.comet_animation(engine)
comet_.color = trail_color
comet_.speed = chase_speed
engine.add_animation(background_)
engine.start_animation(background_)
engine.add_animation(comet_)
engine.start_animation(comet_)
end
animation.register_user_function("comet_chase", comet_chase)
```
#### Template vs User Function Transpilation
**Templates** (DSL-native):
- Defined within DSL files
- Use DSL syntax in body
- Automatically registered
- Type annotations supported
- Transpiled to Berry functions
**User Functions** (Berry-native):
- Defined in Berry code
- Use Berry syntax
- Manually registered
- Full Berry language features
- Called from DSL
### User-Defined Functions
Register custom Berry functions for use in DSL. User functions must take `engine` as the first parameter, followed by any user-provided arguments:

View File

@ -361,6 +361,95 @@ animation a1 = pulsating_animation(color=c1, period=4s)
- Keep animation periods reasonable (>500ms)
- Limit palette sizes for memory efficiency
## Template Examples
Templates provide reusable, parameterized animation patterns that promote code reuse and maintainability.
### 21. Simple Template
```berry
# Define a reusable blinking template
template blink_effect {
param color type color
param speed
param intensity
animation blink = pulsating_animation(
color=color
period=speed
)
blink.opacity = intensity
run blink
}
# Use the template with different parameters
blink_effect(red, 1s, 80%)
blink_effect(blue, 500ms, 100%)
```
### 22. Multi-Animation Template
```berry
# Template that creates a comet chase effect
template comet_chase {
param trail_color type color
param bg_color type color
param chase_speed
param tail_size
# Background layer
animation background = solid(color=bg_color)
background.priority = 1
# Comet effect layer
animation comet = comet_animation(
color=trail_color
tail_length=tail_size
speed=chase_speed
)
comet.priority = 10
run background
run comet
}
# Create different comet effects
comet_chase(white, black, 1500ms, 8)
```
### 23. Template with Dynamic Colors
```berry
# Template using color cycling and breathing effects
template breathing_rainbow {
param cycle_time
param breath_time
param base_brightness
# Create rainbow palette
palette rainbow = [
(0, red), (42, orange), (85, yellow)
(128, green), (170, blue), (213, purple), (255, red)
]
# Create cycling rainbow color
color rainbow_cycle = color_cycle(
palette=rainbow
cycle_period=cycle_time
)
# Create breathing animation with rainbow colors
animation breath = pulsating_animation(
color=rainbow_cycle
period=breath_time
)
breath.opacity = base_brightness
run breath
}
# Use the rainbow breathing template
breathing_rainbow(5s, 2s, 200)
```
## Next Steps
- **[DSL Reference](DSL_REFERENCE.md)** - Complete language syntax

View File

@ -176,9 +176,68 @@ import animation
var runtime = animation.load_dsl_file("my_animation.anim")
```
## User-Defined Functions
## Templates - Reusable Animation Patterns
Create custom animation functions in Berry and use them in DSL:
Templates let you create reusable animation patterns with parameters:
```berry
# Define a template for pulsing effects
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(
color=color
period=speed
)
run pulse
}
# Use the template with different parameters
pulse_effect(red, 2s)
pulse_effect(blue, 1s)
pulse_effect(0xFF69B4, 3s) # Hot pink
```
### Multi-Animation Templates
Templates can contain multiple animations and sequences:
```berry
template comet_chase {
param trail_color type color
param bg_color type color
param chase_speed
# Background glow
animation background = solid_animation(color=bg_color)
# Moving comet
animation comet = comet_animation(
color=trail_color
tail_length=6
speed=chase_speed
)
run background
run comet
}
# Create different comet effects
comet_chase(white, blue, 1500)
comet_chase(orange, black, 2000)
```
**Template Benefits:**
- **Reusable** - Define once, use many times
- **Type Safe** - Optional parameter type checking
- **Clean Syntax** - Pure DSL, no Berry code needed
- **Automatic Registration** - Available immediately after definition
## User-Defined Functions (Advanced)
For complex logic, create custom functions in Berry:
```berry
# Define custom function - engine must be first parameter

View File

@ -268,6 +268,67 @@ end
}
```
7. **Template Definition Errors:**
```berry
# Wrong - missing braces
template pulse_effect
param color type color
param speed
# Error: Expected '{' after template name
# Wrong - invalid parameter syntax
template pulse_effect {
param color as color # Error: Use 'type' instead of 'as'
param speed
}
# Wrong - missing template body
template pulse_effect {
param color type color
}
# Error: Template body cannot be empty
# Correct - proper template syntax
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(
color=color
period=speed
)
run pulse
}
```
8. **Template Call Errors:**
```berry
# Wrong - template not defined
pulse_effect(red, 2s)
# Error: "Undefined reference: 'pulse_effect'"
# Wrong - incorrect parameter count
template pulse_effect {
param color type color
param speed
# ... template body ...
}
pulse_effect(red) # Error: Expected 2 parameters, got 1
# Correct - define template first, call with correct parameters
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(color=color, period=speed)
run pulse
}
pulse_effect(red, 2s) # ✓ Correct usage
```
6. **Parameter Constraint Violations:**
```berry
# Wrong - negative period not allowed
@ -318,7 +379,147 @@ end
}
```
### DSL Runtime Errors
### Template Issues
### Template Definition Problems
**Problem:** Template definitions fail to compile
**Common Template Errors:**
1. **Missing Template Body:**
```berry
# Wrong - empty template
template empty_template {
param color type color
}
# Error: "Template body cannot be empty"
# Correct - template must have content
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(color=color, period=speed)
run pulse
}
```
2. **Invalid Parameter Syntax:**
```berry
# Wrong - old 'as' syntax
template pulse_effect {
param color as color
}
# Error: Expected 'type' keyword, got 'as'
# Correct - use 'type' keyword
template pulse_effect {
param color type color
param speed # Type annotation is optional
}
```
3. **Template Name Conflicts:**
```berry
# Wrong - template name conflicts with built-in function
template solid { # 'solid' is a built-in animation function
param color type color
# ...
}
# Error: "Template name 'solid' conflicts with built-in function"
# Correct - use unique template names
template solid_effect {
param color type color
# ...
}
```
### Template Usage Problems
**Problem:** Template calls fail or behave unexpectedly
**Common Issues:**
1. **Undefined Template:**
```berry
# Wrong - calling undefined template
my_effect(red, 2s)
# Error: "Undefined reference: 'my_effect'"
# Correct - define template first
template my_effect {
param color type color
param speed
# ... template body ...
}
my_effect(red, 2s) # Now works
```
2. **Parameter Count Mismatch:**
```berry
template pulse_effect {
param color type color
param speed
param brightness
}
# Wrong - missing parameters
pulse_effect(red, 2s) # Error: Expected 3 parameters, got 2
# Correct - provide all parameters
pulse_effect(red, 2s, 200)
```
3. **Parameter Type Issues:**
```berry
template pulse_effect {
param color type color
param speed
}
# Wrong - invalid color parameter
pulse_effect("not_a_color", 2s)
# Runtime error: Invalid color value
# Correct - use valid color
pulse_effect(red, 2s) # Named color
pulse_effect(0xFF0000, 2s) # Hex color
```
### Template vs User Function Confusion
**Problem:** Mixing template and user function concepts
**Key Differences:**
```berry
# Template (DSL-native) - Recommended for most cases
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(color=color, period=speed)
run pulse
}
# User Function (Berry-native) - For complex logic
def create_pulse_effect(engine, color, speed)
var pulse = animation.pulsating_animation(engine)
pulse.color = color
pulse.period = speed
return pulse
end
animation.register_user_function("pulse_effect", create_pulse_effect)
```
**When to Use Each:**
- **Templates**: Simple to moderate effects, DSL syntax, type safety
- **User Functions**: Complex logic, Berry features, return values
## DSL Runtime Errors
**Problem:** DSL compiles but fails at runtime
@ -788,6 +989,22 @@ animation red_solid = solid(color=red)
run red_solid
```
### Templates
```berry
# Define reusable template
template pulse_effect {
param color type color
param speed
animation pulse = pulsating_animation(color=color, period=speed)
run pulse
}
# Use template multiple times
pulse_effect(red, 2s)
pulse_effect(blue, 1s)
```
### Animation with Parameters
```berry
color blue = 0x0000FF

View File

@ -30,12 +30,18 @@ animation.register_user_function("breathing", my_breathing)
### 3. Use It in DSL
Call your function just like built-in animations:
First, import your user functions module, then call your function with the `user.` prefix in computed parameters:
```berry
# Use your custom function
animation calm = breathing(blue, 4s)
animation energetic = breathing(red, 1s)
# Import your user functions module
import user_functions
# Use your custom function in computed parameters
animation calm = solid(color=blue)
calm.opacity = user.breathing_effect()
animation energetic = solid(color=red)
energetic.opacity = user.breathing_effect()
sequence demo {
play calm for 10s
@ -45,6 +51,91 @@ sequence demo {
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 with the user. prefix
animation test = solid(color=blue)
test.opacity = user.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 = user.rand_demo()
# User function with parameters
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = user.breathing(128, 64)
# User functions in mathematical expressions
animation complex = solid(color=green)
complex.opacity = max(50, min(255, user.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=user.random_color())
base.opacity = user.breathing(200, 50)
animation flames = solid(color=red)
flames.opacity = user.fire_intensity(180)
```
## Common Patterns
### Simple Color Effects
@ -61,8 +152,11 @@ animation.register_user_function("bright", solid_bright)
```
```berry
animation bright_red = bright(red, 80%)
animation dim_blue = bright(blue, 30%)
animation bright_red = solid(color=red)
bright_red.opacity = user.bright(80)
animation dim_blue = solid(color=blue)
dim_blue.opacity = user.bright(30)
```
### Fire Effects
@ -83,8 +177,11 @@ animation.register_user_function("fire", custom_fire)
```
```berry
animation campfire = fire(200, 2s)
animation torch = fire(255, 500ms)
animation campfire = solid(color=red)
campfire.opacity = user.fire(200, 2000)
animation torch = solid(color=orange)
torch.opacity = user.fire(255, 500)
```
### Sparkle Effects
@ -102,8 +199,11 @@ animation.register_user_function("sparkles", sparkles)
```
```berry
animation stars = sparkles(white, 12, 300ms)
animation fairy_dust = sparkles(#FFD700, 8, 500ms)
animation stars = solid(color=white)
stars.opacity = user.sparkles(12, 300)
animation fairy_dust = solid(color=#FFD700)
fairy_dust.opacity = user.sparkles(8, 500)
```
### Position-Based Effects
@ -122,8 +222,11 @@ animation.register_user_function("pulse_at", pulse_at)
```
```berry
animation left_pulse = pulse_at(green, 5, 3, 2s)
animation right_pulse = pulse_at(blue, 25, 3, 2s)
animation left_pulse = solid(color=green)
left_pulse.position = user.pulse_at(5, 3, 2000)
animation right_pulse = solid(color=blue)
right_pulse.position = user.pulse_at(25, 3, 2000)
```
## Advanced Examples
@ -175,9 +278,14 @@ animation.register_user_function("alert", gentle_alert)
```
```berry
animation emergency = strobe()
animation notification = alert()
animation custom_police = police(500ms)
animation emergency = solid(color=red)
emergency.opacity = user.strobe()
animation notification = solid(color=yellow)
notification.opacity = user.alert()
animation custom_police = solid(color=blue)
custom_police.opacity = user.police(500)
```
## Function Organization
@ -302,7 +410,7 @@ User functions can be used in computed parameter expressions alongside mathemati
```berry
# Simple user function call in property assignment
animation base = solid(color=blue, priority=10)
base.opacity = rand_demo() # User function as computed parameter
base.opacity = user.rand_demo() # User function as computed parameter
```
### User Functions with Mathematical Operations
@ -314,7 +422,7 @@ 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
opacity=max(50, min(255, user.rand_demo() + 100)) # Random opacity with bounds
priority=15
)
```
@ -325,7 +433,7 @@ animation dynamic_solid = solid(
# Use user function in arithmetic expressions
animation random_effect = solid(
color=cyan
opacity=abs(rand_demo() - 128) + 64 # Random variation around middle value
opacity=abs(user.rand_demo() - 128) + 64 # Random variation around middle value
priority=12
)
```
@ -342,7 +450,7 @@ When you use user functions in computed parameters:
**Generated Code Example:**
```berry
# DSL code
animation.opacity = max(100, breathing(red, 2000))
animation.opacity = max(100, user.breathing(red, 2000))
```
**Transpiles to:**
@ -359,7 +467,7 @@ The following user functions are available by default:
| Function | Parameters | Description |
|----------|------------|-------------|
| `rand_demo()` | none | Returns a random value (0-255) for demonstration |
| `user.rand_demo()` | none | Returns a random value (0-255) for demonstration |
### Best Practices for Computed Parameters
@ -378,16 +486,20 @@ import animation
# Load your custom functions
load("user_animations.be")
# Now they're available in DSL
# Now they're available in DSL with import
var dsl_code =
"animation my_fire = fire(200, 1500ms)\n"
"animation my_sparkles = sparkle(white, 8, 400ms)\n"
"import user_functions\n"
"\n"
"animation my_fire = solid(color=red)\n"
"my_fire.opacity = user.fire(200, 1500)\n"
"animation my_sparkles = solid(color=white)\n"
"my_sparkles.opacity = user.sparkle(8, 400)\n"
"\n"
"sequence show {\n"
" play my_fire for 10s\n"
" play my_sparkles for 5s\n"
"}\n"
"\n
"\n"
"run show"
animation_dsl.execute(dsl_code)
@ -398,8 +510,12 @@ animation_dsl.execute(dsl_code)
```berry
# Save DSL with custom functions
var my_show =
"animation campfire = fire(180, 2s)\n"
"animation stars = sparkle(#FFFFFF, 6, 600ms)\n"
"import user_functions\n"
"\n"
"animation campfire = solid(color=orange)\n"
"campfire.opacity = user.fire(180, 2000)\n"
"animation stars = solid(color=#FFFFFF)\n"
"stars.opacity = user.sparkle(6, 600)\n"
"\n"
"sequence night_scene {\n"
" play campfire for 30s\n"

View File

@ -87,12 +87,12 @@ import "core/user_functions" as user_functions
register_to_animation(user_functions)
# Import and register actual user functions
try
import "user_functions" as user_funcs # This registers the actual user functions
except .. as e, msg
# User functions are optional - continue without them if not available
print(f"Note: User functions not loaded: {msg}")
end
# try
# import "user_functions" as user_funcs # This registers the actual user functions
# except .. as e, msg
# # User functions are optional - continue without them if not available
# print(f"Note: User functions not loaded: {msg}")
# end
# Import value providers
import "providers/value_provider.be" as value_provider
@ -201,28 +201,6 @@ def animation_init_strip(*l)
end
animation.init_strip = animation_init_strip
# Global variable resolver with error checking
# Used by DSL-generated code to resolve variable names during execution
# First checks animation module, then global scope for user-defined variables
def animation_global(name, module_name)
import global
import introspect
import animation
# First try to find in animation module (built-in functions/classes)
if (module_name != nil) && introspect.contains(animation, module_name)
return animation.(module_name)
end
# Then try global scope (user-defined variables)
if global.contains(name)
return global.(name)
else
raise "syntax_error", f"'{name}' undeclared"
end
end
animation.global = animation_global
# This function is called from C++ code to set up the Berry animation environment
# It creates a mutable 'animation' module on top of the immutable solidified
#
@ -250,6 +228,9 @@ def animation_init(m)
end
end
# Create an empty map for user_functions
animation_new._user_functions = {}
return animation_new
end
animation.init = animation_init

View File

@ -184,7 +184,8 @@ class FireAnimation : animation.animation
fire_provider.cycle_period = 0 # Use value-based color mapping, not time-based
fire_provider.transition_type = 1 # Use sine transition (smooth)
fire_provider.brightness = 255
fire_provider.set_range(0, 255)
fire_provider.range_min = 0
fire_provider.range_max = 255
resolved_color = fire_provider
end

View File

@ -8,7 +8,7 @@
#@ solidify:PalettePatternAnimation,weak
class PalettePatternAnimation : animation.animation
var value_buffer # Buffer to store values for each pixel
var value_buffer # Buffer to store values for each pixel (bytes object)
# Static definitions of parameters with constraints
static var PARAMS = {
@ -25,7 +25,7 @@ class PalettePatternAnimation : animation.animation
super(self).init(engine)
# Initialize non-parameter instance variables only
self.value_buffer = []
self.value_buffer = bytes()
# Initialize value buffer with default frame width
self._initialize_value_buffer()
@ -63,7 +63,12 @@ class PalettePatternAnimation : animation.animation
# Calculate values for each pixel
var i = 0
while i < strip_length
self.value_buffer[i] = pattern_func(i, time_ms, self)
var pattern_value = pattern_func(i, time_ms, self)
# Pattern function should return values in 0-255 range, clamp to byte range
var byte_value = int(pattern_value)
if byte_value < 0 byte_value = 0 end
if byte_value > 255 byte_value = 255 end
self.value_buffer[i] = byte_value
i += 1
end
end
@ -103,11 +108,16 @@ class PalettePatternAnimation : animation.animation
end
# Get current parameter values (cached for performance)
var color_source = self.color_source
var color_source = self.get_param('color_source') # use get_param to avoid resolving of color_provider
if color_source == nil
return false
end
# Check if color_source has the required method (more flexible than isinstance check)
if color_source.get_color_for_value == nil
return false
end
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
@ -115,17 +125,10 @@ class PalettePatternAnimation : animation.animation
var strip_length = self.engine.get_strip_length()
var i = 0
while i < strip_length && i < frame.width
var value = self.value_buffer[i]
var color
var byte_value = self.value_buffer[i]
# Check if color_source is a ColorProvider or an animation with get_color_for_value method
if color_source.get_color_for_value != nil
# It's a ColorProvider or compatible object
color = color_source.get_color_for_value(value, elapsed)
else
# Fallback to direct color access (for backward compatibility)
color = color_source.current_color
end
# Use the color_source to get color for the byte value (0-255)
var color = color_source.get_color_for_value(byte_value, elapsed)
frame.set_pixel_color(i, color)
i += 1
@ -191,13 +194,14 @@ class PaletteWaveAnimation : PalettePatternAnimation
# Calculate values for each pixel
var i = 0
while i < strip_length
# Calculate the wave value (0-100) using scale_uint
# Calculate the wave value (0-255) using scale_uint
var pos_in_wave = (i + offset) % wave_length
var angle = tasmota.scale_uint(pos_in_wave, 0, wave_length, 0, 32767) # 0 to 2π in fixed-point
var sine_value = tasmota.sine_int(angle) # -4096 to 4096
# Map sine value from -4096..4096 to 0..100
self.value_buffer[i] = tasmota.scale_int(sine_value, -4096, 4096, 0, 100)
# Map sine value from -4096..4096 to 0..255
var byte_value = tasmota.scale_int(sine_value, -4096, 4096, 0, 255)
self.value_buffer[i] = byte_value
i += 1
end
end
@ -209,7 +213,9 @@ class PaletteGradientAnimation : PalettePatternAnimation
# Static definitions of parameters with constraints
static var PARAMS = {
# Gradient-specific parameters only
"shift_period": {"min": 1, "default": 10000}
"shift_period": {"min": 0, "default": 0}, # Time for one complete shift cycle in ms (0 = static)
"spatial_period": {"min": 0, "default": 0}, # Spatial period in pixels (0 = full strip)
"phase_shift": {"min": 0, "max": 100, "default": 0} # Phase shift as percentage (0-100)
}
# Initialize a new gradient pattern animation
@ -227,6 +233,8 @@ class PaletteGradientAnimation : PalettePatternAnimation
def _update_value_buffer(time_ms)
# Cache parameter values for performance
var shift_period = self.shift_period
var spatial_period = self.spatial_period
var phase_shift = self.phase_shift
var strip_length = self.engine.get_strip_length()
# Resize buffer if strip length changed
@ -234,16 +242,28 @@ class PaletteGradientAnimation : PalettePatternAnimation
self.value_buffer.resize(strip_length)
end
# Calculate the shift position using scale_uint for better precision
var position = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, 1000) / 1000.0
var offset = int(position * strip_length)
# Determine effective spatial period (0 means full strip)
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
# Calculate the temporal shift position (how much the pattern has moved over time)
var temporal_offset = 0
if shift_period > 0
var temporal_position = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, 1000) / 1000.0
temporal_offset = temporal_position * effective_spatial_period
end
# Calculate the phase shift offset in pixels
var phase_offset = tasmota.scale_uint(phase_shift, 0, 100, 0, effective_spatial_period)
# Calculate values for each pixel
var i = 0
while i < strip_length
# Calculate the gradient value (0-100) using scale_uint
var pos_in_frame = (i + offset) % strip_length
self.value_buffer[i] = tasmota.scale_uint(pos_in_frame, 0, strip_length - 1, 0, 100)
# Calculate position within the spatial period, including temporal and phase offsets
var spatial_pos = (i + temporal_offset + phase_offset) % effective_spatial_period
# Map spatial position to gradient value (0-255)
var byte_value = tasmota.scale_uint(int(spatial_pos), 0, effective_spatial_period - 1, 0, 255)
self.value_buffer[i] = byte_value
i += 1
end
end
@ -293,8 +313,8 @@ class PaletteMeterAnimation : PalettePatternAnimation
# Calculate values for each pixel
var i = 0
while i < strip_length
# Return 100 if pixel is within the meter, 0 otherwise
self.value_buffer[i] = i < meter_position ? 100 : 0
# Return 255 if pixel is within the meter, 0 otherwise
self.value_buffer[i] = i < meter_position ? 255 : 0
i += 1
end
end

View File

@ -243,7 +243,8 @@ def wave_rainbow_sine(engine)
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1 # sine transition
rainbow_provider.brightness = 255
rainbow_provider.set_range(0, 255)
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
anim.color = rainbow_provider
anim.wave_type = 0 # sine wave
anim.frequency = 32

View File

@ -11,16 +11,17 @@ class Animation : animation.parameterized_object
# Non-parameter instance variables only
var start_time # Time when animation started (ms) (int)
var current_time # Current animation time (ms) (int)
var opacity_frame # Frame buffer for opacity animation rendering
# Parameter definitions
static var PARAMS = {
"name": {"type": "string", "default": "animation"}, # Optional name for the animation
"is_running": {"type": "bool", "default": false}, # Whether the animation is active
"priority": {"min": 0, "default": 10}, # Rendering priority (higher = on top, 0-255)
"duration": {"min": 0, "default": 0}, # Animation duration in ms (0 = infinite)
"loop": {"type": "bool", "default": true}, # Whether to loop when duration is reached
"opacity": {"min": 0, "max": 255, "default": 255}, # Animation opacity/brightness (0-255)
"color": {"default": 0xFFFFFFFF} # Base color in ARGB format (0xAARRGGBB)
"name": {"type": "string", "default": "animation"}, # Optional name for the animation
"is_running": {"type": "bool", "default": false}, # Whether the animation is active
"priority": {"min": 0, "default": 10}, # Rendering priority (higher = on top, 0-255)
"duration": {"min": 0, "default": 0}, # Animation duration in ms (0 = infinite)
"loop": {"type": "bool", "default": false}, # Whether to loop when duration is reached
"opacity": {"type": "any", "default": 255}, # Animation opacity (0-255 number or Animation instance)
"color": {"default": 0xFFFFFFFF} # Base color in ARGB format (0xAARRGGBB)
}
# Initialize a new animation
@ -33,6 +34,7 @@ class Animation : animation.parameterized_object
# Initialize non-parameter instance variables
self.start_time = 0
self.current_time = 0
self.opacity_frame = nil # Will be created when needed
end
# Start/restart the animation (make it active and reset timing)
@ -140,6 +142,11 @@ class Animation : animation.parameterized_object
return false
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Update animation state
self.update(time_ms)
@ -147,17 +154,54 @@ class Animation : animation.parameterized_object
var current_color = self.color
var current_opacity = self.opacity
# Fill the entire frame with the current color
frame.fill_pixels(current_color)
# Apply resolved opacity if not full
if current_opacity < 255
frame.apply_brightness(current_opacity)
# Fill the entire frame with the current color if not transparent
if (current_color != 0x00000000)
frame.fill_pixels(current_color)
end
# Handle opacity - can be number, frame buffer, or animation
self._apply_opacity(frame, current_opacity, time_ms)
return true
end
# Apply opacity to frame buffer - handles numbers and animations
#
# @param frame: FrameBuffer - The frame buffer to apply opacity to
# @param opacity: int|Animation - Opacity value or animation
# @param time_ms: int - Current time in milliseconds
def _apply_opacity(frame, opacity, time_ms)
# Check if opacity is an animation instance
if isinstance(opacity, animation.animation)
# Animation mode: render opacity animation to frame buffer and use as mask
var opacity_animation = opacity
# Ensure opacity frame buffer exists and has correct size
if self.opacity_frame == nil || self.opacity_frame.width != frame.width
self.opacity_frame = animation.frame_buffer(frame.width)
end
# Clear and render opacity animation to frame buffer
self.opacity_frame.clear()
# Start opacity animation if not running
if !opacity_animation.is_running
opacity_animation.start(self.start_time)
end
# Update and render opacity animation
opacity_animation.update(time_ms)
opacity_animation.render(self.opacity_frame, time_ms)
# Use rendered frame buffer as opacity mask
frame.apply_opacity(self.opacity_frame)
elif type(opacity) == 'int' && opacity < 255
# Number mode: apply uniform opacity
frame.apply_opacity(opacity)
end
# If opacity is 255 (full opacity), do nothing
end
# Get a color for a specific pixel position and time
# Default implementation returns the animation's color (solid color for all pixels)
#

View File

@ -485,68 +485,92 @@ class FrameBuffer
end
# Apply an opacity adjustment to the frame buffer
# opacity: opacity factor (0-511, where 0 is fully transparent, 255 is original, 511 is maximum opaque)
# start_pos: start position (default: 0)
# end_pos: end position (default: width-1)
def apply_opacity(opacity, start_pos, end_pos)
# opacity: opacity factor (0-511) or another FrameBuffer to use as mask
# - Number: 0 is fully transparent, 255 is original, 511 is maximum opaque
# - FrameBuffer: uses alpha channel as opacity mask
def apply_opacity(opacity)
if opacity == nil
opacity = 255
end
if start_pos == nil
start_pos = 0
end
if end_pos == nil
end_pos = self.width - 1
end
# Validate parameters
if start_pos < 0 || start_pos >= self.width
raise "index_error", "start_pos out of range"
end
if end_pos < start_pos || end_pos >= self.width
raise "index_error", "end_pos out of range"
end
# Ensure opacity is in valid range (0-511)
opacity = opacity < 0 ? 0 : (opacity > 511 ? 511 : opacity)
# Apply opacity adjustment
var i = start_pos
while i <= end_pos
var color = self.get_pixel_color(i)
# Check if opacity is a FrameBuffer (mask mode)
if isinstance(opacity, animation.frame_buffer)
# Mask mode: use another frame buffer as opacity mask
var mask_buffer = opacity
# Extract components (ARGB format - 0xAARRGGBB)
var a = (color >> 24) & 0xFF
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Adjust alpha using tasmota.scale_uint
# For opacity 0-255: scale down alpha
# For opacity 256-511: scale up alpha (but cap at 255)
if opacity <= 255
a = tasmota.scale_uint(opacity, 0, 255, 0, a)
else
# Scale up alpha: map 256-511 to 1.0-2.0 multiplier
a = tasmota.scale_uint(a * opacity, 0, 255 * 255, 0, 255)
a = a > 255 ? 255 : a # Cap at maximum alpha
if self.width != mask_buffer.width
raise "value_error", "frame buffers must have the same width"
end
# Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)
color = (a << 24) | (r << 16) | (g << 8) | b
var i = 0
while i < self.width
var color = self.get_pixel_color(i)
var mask_color = mask_buffer.get_pixel_color(i)
# Extract alpha from mask as opacity factor (0-255)
var mask_opacity = (mask_color >> 24) & 0xFF
# Extract components from color (ARGB format - 0xAARRGGBB)
var a = (color >> 24) & 0xFF
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Apply mask opacity to alpha channel using tasmota.scale_uint
a = tasmota.scale_uint(mask_opacity, 0, 255, 0, a)
# Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)
var new_color = (a << 24) | (r << 16) | (g << 8) | b
# Update the pixel
self.set_pixel_color(i, new_color)
i += 1
end
else
# Number mode: uniform opacity adjustment
var opacity_value = int(opacity)
# Update the pixel
self.set_pixel_color(i, color)
# Ensure opacity is in valid range (0-511)
opacity_value = opacity_value < 0 ? 0 : (opacity_value > 511 ? 511 : opacity_value)
i += 1
# Apply opacity adjustment
var i = 0
while i < self.width
var color = self.get_pixel_color(i)
# Extract components (ARGB format - 0xAARRGGBB)
var a = (color >> 24) & 0xFF
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Adjust alpha using tasmota.scale_uint
# For opacity 0-255: scale down alpha
# For opacity 256-511: scale up alpha (but cap at 255)
if opacity_value <= 255
a = tasmota.scale_uint(opacity_value, 0, 255, 0, a)
else
# Scale up alpha: map 256-511 to 1.0-2.0 multiplier
a = tasmota.scale_uint(a * opacity_value, 0, 255 * 255, 0, 255)
a = a > 255 ? 255 : a # Cap at maximum alpha
end
# Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)
color = (a << 24) | (r << 16) | (g << 8) | b
# Update the pixel
self.set_pixel_color(i, color)
i += 1
end
end
end
# Apply a brightness adjustment to the frame buffer
# brightness: brightness factor (0-511, where 0 is black, 255 is original, and 511 is maximum bright)
# brightness: brightness factor (0-511) or another FrameBuffer to use as mask
# - Number: 0 is black, 255 is original, 511 is maximum bright
# - FrameBuffer: uses alpha channel as brightness mask
# start_pos: start position (default: 0)
# end_pos: end position (default: width-1)
def apply_brightness(brightness, start_pos, end_pos)
@ -571,45 +595,86 @@ class FrameBuffer
raise "index_error", "end_pos out of range"
end
# Ensure brightness is in valid range (0-511)
brightness = brightness < 0 ? 0 : (brightness > 511 ? 511 : brightness)
# Apply brightness adjustment
var i = start_pos
while i <= end_pos
var color = self.get_pixel_color(i)
# Check if brightness is a FrameBuffer (mask mode)
if isinstance(brightness, animation.frame_buffer)
# Mask mode: use another frame buffer as brightness mask
var mask_buffer = brightness
# Extract components (ARGB format - 0xAARRGGBB)
var a = (color >> 24) & 0xFF
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Adjust brightness using tasmota.scale_uint
# For brightness 0-255: scale down RGB
# For brightness 256-511: scale up RGB (but cap at 255)
if brightness <= 255
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
b = tasmota.scale_uint(b, 0, 255, 0, brightness)
else
# Scale up RGB: map 256-511 to 1.0-2.0 multiplier
var multiplier = brightness - 255 # 0-256 range
r = r + tasmota.scale_uint(r * multiplier, 0, 255 * 256, 0, 255)
g = g + tasmota.scale_uint(g * multiplier, 0, 255 * 256, 0, 255)
b = b + tasmota.scale_uint(b * multiplier, 0, 255 * 256, 0, 255)
r = r > 255 ? 255 : r # Cap at maximum
g = g > 255 ? 255 : g # Cap at maximum
b = b > 255 ? 255 : b # Cap at maximum
if self.width != mask_buffer.width
raise "value_error", "frame buffers must have the same width"
end
# Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)
color = (a << 24) | (r << 16) | (g << 8) | b
var i = start_pos
while i <= end_pos
var color = self.get_pixel_color(i)
var mask_color = mask_buffer.get_pixel_color(i)
# Extract alpha from mask as brightness factor (0-255)
var mask_brightness = (mask_color >> 24) & 0xFF
# Extract components from color (ARGB format - 0xAARRGGBB)
var a = (color >> 24) & 0xFF
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Apply mask brightness to RGB channels using tasmota.scale_uint
r = tasmota.scale_uint(mask_brightness, 0, 255, 0, r)
g = tasmota.scale_uint(mask_brightness, 0, 255, 0, g)
b = tasmota.scale_uint(mask_brightness, 0, 255, 0, b)
# Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)
var new_color = (a << 24) | (r << 16) | (g << 8) | b
# Update the pixel
self.set_pixel_color(i, new_color)
i += 1
end
else
# Number mode: uniform brightness adjustment
var brightness_value = int(brightness)
# Update the pixel
self.set_pixel_color(i, color)
# Ensure brightness is in valid range (0-511)
brightness_value = brightness_value < 0 ? 0 : (brightness_value > 511 ? 511 : brightness_value)
i += 1
# Apply brightness adjustment
var i = start_pos
while i <= end_pos
var color = self.get_pixel_color(i)
# Extract components (ARGB format - 0xAARRGGBB)
var a = (color >> 24) & 0xFF
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Adjust brightness using tasmota.scale_uint
# For brightness 0-255: scale down RGB
# For brightness 256-511: scale up RGB (but cap at 255)
if brightness_value <= 255
r = tasmota.scale_uint(r, 0, 255, 0, brightness_value)
g = tasmota.scale_uint(g, 0, 255, 0, brightness_value)
b = tasmota.scale_uint(b, 0, 255, 0, brightness_value)
else
# Scale up RGB: map 256-511 to 1.0-2.0 multiplier
var multiplier = brightness_value - 255 # 0-256 range
r = r + tasmota.scale_uint(r * multiplier, 0, 255 * 256, 0, 255)
g = g + tasmota.scale_uint(g * multiplier, 0, 255 * 256, 0, 255)
b = b + tasmota.scale_uint(b * multiplier, 0, 255 * 256, 0, 255)
r = r > 255 ? 255 : r # Cap at maximum
g = g > 255 ? 255 : g # Cap at maximum
b = b > 255 ? 255 : b # Cap at maximum
end
# Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)
color = (a << 24) | (r << 16) | (g << 8) | b
# Update the pixel
self.set_pixel_color(i, color)
i += 1
end
end
end

View File

@ -3,33 +3,25 @@
#@ solidify:animation_user_functions,weak
# Module-level storage for user-defined functions
import global
global._animation_user_functions = {}
# Register a Berry function for DSL use
def register_user_function(name, func)
import global
global._animation_user_functions[name] = func
animation._user_functions[name] = func
end
# Retrieve a registered function by name
def get_user_function(name)
import global
return global._animation_user_functions.find(name)
return animation._user_functions.find(name)
end
# Check if a function is registered
def is_user_function(name)
import global
return global._animation_user_functions.contains(name)
return animation._user_functions.contains(name)
end
# List all registered function names
def list_user_functions()
import global
var names = []
for name : global._animation_user_functions.keys()
for name : animation.user_functions.keys()
names.push(name)
end
return names

View File

@ -27,15 +27,15 @@ class Token
static var statement_keywords = [
"strip", "set", "color", "palette", "animation",
"sequence", "function", "zone", "on", "run"
"sequence", "function", "zone", "on", "run", "template", "param", "import"
]
static var keywords = [
# Configuration keywords
"strip", "set",
"strip", "set", "import",
# Definition keywords
"color", "palette", "animation", "sequence", "function", "zone",
"color", "palette", "animation", "sequence", "function", "zone", "template", "param", "type",
# Control flow keywords
"play", "for", "with", "repeat", "times", "forever", "if", "else", "elif",

View File

@ -27,6 +27,8 @@ class SimpleDSLTranspiler
var sequence_names # Track which names are sequences
var symbol_table # Track created objects: name -> instance
var indent_level # Track current indentation level for nested sequences
var template_definitions # Track template definitions: name -> {params, body}
var has_template_calls # Track if we have template calls to trigger engine.start()
# Static color mapping for named colors (helps with solidification)
static var named_colors = {
@ -56,17 +58,31 @@ class SimpleDSLTranspiler
self.sequence_names = {} # Track which names are sequences
self.symbol_table = {} # Track created objects: name -> instance
self.indent_level = 0 # Track current indentation level
self.template_definitions = {} # Track template definitions
self.has_template_calls = false # Track if we have template calls
end
# Get current indentation string
def get_indent()
# return " " * (self.indent_level + 1) # Base indentation is 2 spaces - string multiplication not supported
var indent = ""
var spaces_needed = (self.indent_level + 1) * 2 # Base indentation is 2 spaces
for i : 0..spaces_needed-1
indent += " "
return " " * (self.indent_level + 1) # Base indentation is 2 spaces
end
# Helper method to process user function calls (user.function_name())
def _process_user_function_call(func_name)
# Check if this is a function call (user.function_name())
if self.current() != nil && self.current().type == animation_dsl.Token.LEFT_PAREN
# This is a user function call: user.function_name()
# Don't check for existence during transpilation - trust that function will be available at runtime
# User functions use positional parameters with engine as first argument
# In closure context, use self.engine to access the engine from the ClosureValueProvider
var args = self.process_function_arguments_for_expression()
var full_args = args != "" ? f"self.engine, {args}" : "self.engine"
return f"animation.get_user_function('{func_name}')({full_args})"
else
self.error("User functions must be called with parentheses: user.function_name()")
return "nil"
end
return indent
end
# Main transpilation method - single pass
@ -90,6 +106,31 @@ class SimpleDSLTranspiler
end
end
# Transpile template body (similar to main transpile but without imports/engine start)
def transpile_template_body()
try
# Process all statements in template body
while !self.at_end()
self.process_statement()
end
# For templates, process run statements immediately instead of collecting them
if size(self.run_statements) > 0
for run_stmt : self.run_statements
var obj_name = run_stmt["name"]
var comment = run_stmt["comment"]
# In templates, use underscore suffix for local variables
self.add(f"engine.add_animation({obj_name}_){comment}")
end
end
return size(self.errors) == 0 ? self.join_output() : nil
except .. as e, msg
self.error(f"Template body transpilation failed: {msg}")
return nil
end
end
# Process statements - simplified approach
def process_statement()
var tok = self.current()
@ -139,8 +180,12 @@ class SimpleDSLTranspiler
self.process_set()
elif tok.value == "sequence"
self.process_sequence()
elif tok.value == "template"
self.process_template()
elif tok.value == "run"
self.process_run()
elif tok.value == "import"
self.process_import()
elif tok.value == "on"
self.process_event_handler()
else
@ -188,12 +233,12 @@ class SimpleDSLTranspiler
self.next()
end
# Check if this is a user-defined function
if animation.is_user_function(func_name)
# User functions use positional parameters with engine as first argument
# Check if this is a template call first
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
var args = self.process_function_arguments()
var full_args = args != "" ? f"engine, {args}" : "engine"
self.add(f"var {name}_ = animation.get_user_function('{func_name}')({full_args}){inline_comment}")
self.add(f"{func_name}_template({full_args}){inline_comment}")
else
# Built-in functions use the new engine-first + named parameters pattern
# Validate that the factory function exists at transpilation time
@ -382,12 +427,12 @@ class SimpleDSLTranspiler
self.next()
end
# Check if this is a user-defined function
if animation.is_user_function(func_name)
# User functions use positional parameters with engine as first argument
# Check if this is a template call first
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
var args = self.process_function_arguments()
var full_args = args != "" ? f"engine, {args}" : "engine"
self.add(f"var {name}_ = animation.get_user_function('{func_name}')({full_args}){inline_comment}")
self.add(f"{func_name}_template({full_args}){inline_comment}")
else
# Built-in functions use the new engine-first + named parameters pattern
# Validate that the factory function creates an animation instance at transpile time
@ -430,8 +475,6 @@ class SimpleDSLTranspiler
self.symbol_table[name] = ref_instance
end
end
# Note: For identifier references, type checking happens at runtime via animation.global()
end
end
@ -469,6 +512,103 @@ class SimpleDSLTranspiler
self.symbol_table[name] = "variable"
end
# Process template definition: template name { param ... }
def process_template()
self.next() # skip 'template'
var name = self.expect_identifier()
# Validate that the template name is not reserved
if !self.validate_user_name(name, "template")
self.skip_statement()
return
end
self.expect_left_brace()
# First pass: collect all parameters
var params = []
var param_types = {}
while !self.at_end() && !self.check_right_brace()
self.skip_whitespace_including_newlines()
if self.check_right_brace()
break
end
var tok = self.current()
if tok != nil && tok.type == animation_dsl.Token.KEYWORD && tok.value == "param"
# Process parameter declaration
self.next() # skip 'param'
var param_name = self.expect_identifier()
# Check for optional type annotation
var param_type = nil
if self.current() != nil && self.current().type == animation_dsl.Token.KEYWORD && self.current().value == "type"
self.next() # skip 'type'
param_type = self.expect_identifier()
end
params.push(param_name)
if param_type != nil
param_types[param_name] = param_type
end
# Skip optional newline after parameter
if self.current() != nil && self.current().type == animation_dsl.Token.NEWLINE
self.next()
end
else
# Found non-param statement, break to collect body
break
end
end
# Second pass: collect body tokens (everything until closing brace)
var body_tokens = []
var brace_depth = 0
while !self.at_end() && !self.check_right_brace()
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
break
end
if tok.type == animation_dsl.Token.LEFT_BRACE
brace_depth += 1
body_tokens.push(tok)
elif tok.type == animation_dsl.Token.RIGHT_BRACE
if brace_depth == 0
break # This is our closing brace
else
brace_depth -= 1
body_tokens.push(tok)
end
else
body_tokens.push(tok)
end
self.next()
end
self.expect_right_brace()
# Store template definition
self.template_definitions[name] = {
'params': params,
'param_types': param_types,
'body_tokens': body_tokens
}
# Generate Berry function for this template
self.generate_template_function(name, params, param_types, body_tokens)
# Add template to symbol table as a special marker
self.symbol_table[name] = "template"
end
# Process sequence definition: sequence demo { ... } or sequence demo repeat N times { ... }
def process_sequence()
self.next() # skip 'sequence'
@ -546,75 +686,6 @@ class SimpleDSLTranspiler
self.expect_right_brace()
end
# Process statements inside sequences using push_step()
def process_sequence_statement_for_manager(manager_name)
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
return
end
# Handle comments - preserve them in generated code with proper indentation
if tok.type == animation_dsl.Token.COMMENT
self.add(" " + tok.value) # Add comment with sequence indentation
self.next()
return
end
# Skip whitespace (newlines)
if tok.type == animation_dsl.Token.NEWLINE
self.next()
return
end
if tok.type == animation_dsl.Token.KEYWORD && tok.value == "play"
self.process_play_statement_for_manager(manager_name)
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "wait"
self.process_wait_statement_for_manager(manager_name)
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "repeat"
self.next() # skip 'repeat'
# Parse repeat count: either number or "forever"
var repeat_count = "1"
var tok_after_repeat = self.current()
if tok_after_repeat != nil && tok_after_repeat.type == animation_dsl.Token.KEYWORD && tok_after_repeat.value == "forever"
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
else
var count = self.expect_number()
self.expect_keyword("times")
repeat_count = str(count)
end
self.expect_left_brace()
# Create repeat sub-sequence
self.add(f" var repeat_seq = animation.SequenceManager(engine, {repeat_count})")
# Process repeat body - add steps directly to repeat sequence
while !self.at_end() && !self.check_right_brace()
self.process_sequence_statement_for_manager("repeat_seq")
end
self.expect_right_brace()
# Add the repeat sub-sequence step to main sequence
self.add(f" {manager_name}.push_repeat_subsequence(repeat_seq.steps, {repeat_count})")
elif tok.type == animation_dsl.Token.IDENTIFIER
# Check if this is a property assignment (identifier.property = value)
if self.peek() != nil && self.peek().type == animation_dsl.Token.DOT
self.process_sequence_assignment_for_manager(" ", manager_name) # Pass indentation and manager name
else
self.skip_statement()
end
else
self.skip_statement()
end
end
# Process statements inside sequences using fluent interface
def process_sequence_statement()
var tok = self.current()
@ -799,89 +870,19 @@ class SimpleDSLTranspiler
var inline_comment = self.collect_inline_comment()
self.add(f"{self.get_indent()}.push_wait_step({duration}){inline_comment}")
end
# Helper method to process play statement with configurable target array (legacy)
def process_play_statement(target_array)
self.next() # skip 'play'
# Check if this is a function call or an identifier
var anim_ref = ""
var current_tok = self.current()
if current_tok != nil && (current_tok.type == animation_dsl.Token.IDENTIFIER || current_tok.type == animation_dsl.Token.KEYWORD) &&
self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
# This is a function call - process it as a nested function call
anim_ref = self.process_nested_function_call()
else
# This is an identifier reference - sequences need runtime resolution
var anim_name = self.expect_identifier()
# Validate that the referenced object exists
self._validate_object_reference(anim_name, "sequence play")
anim_ref = f"animation.global('{anim_name}_')"
end
# Handle optional 'for duration'
var duration = "0"
if self.current() != nil && self.current().type == animation_dsl.Token.KEYWORD && self.current().value == "for"
self.next() # skip 'for'
duration = str(self.process_time_value())
end
# Process import statement: import user_functions or import module_name
def process_import()
self.next() # skip 'import'
var module_name = self.expect_identifier()
var inline_comment = self.collect_inline_comment()
self.add(f" {target_array}.push(animation.create_play_step({anim_ref}, {duration})){inline_comment}")
# Generate Berry import statement with quoted module name
self.add(f'import {module_name} {inline_comment}')
end
# Helper method to process wait statement with configurable target array (legacy)
def process_wait_statement(target_array)
self.next() # skip 'wait'
var duration = self.process_time_value()
var inline_comment = self.collect_inline_comment()
self.add(f" {target_array}.push(animation.create_wait_step({duration})){inline_comment}")
end
# Generic method to process sequence statements with configurable target array
def process_sequence_statement_generic(target_array)
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
return
end
# Handle comments - preserve them in generated code with proper indentation
if tok.type == animation_dsl.Token.COMMENT
self.add(" " + tok.value) # Add comment with sequence indentation
self.next()
return
end
# Skip whitespace (newlines)
if tok.type == animation_dsl.Token.NEWLINE
self.next()
return
end
if tok.type == animation_dsl.Token.KEYWORD && tok.value == "play"
self.process_play_statement(target_array)
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "wait"
self.process_wait_statement(target_array)
elif tok.type == animation_dsl.Token.IDENTIFIER
# Check if this is a property assignment (identifier.property = value)
if self.peek() != nil && self.peek().type == animation_dsl.Token.DOT
self.process_sequence_assignment_generic(" ", target_array) # Pass indentation and target array
else
self.skip_statement()
end
else
self.skip_statement()
end
end
# Process run statement: run demo
def process_run()
self.next() # skip 'run'
@ -899,11 +900,29 @@ class SimpleDSLTranspiler
})
end
# Process property assignment: animation_name.property = value
# Process property assignment or standalone function call: animation_name.property = value OR template_call(args)
def process_property_assignment()
var object_name = self.expect_identifier()
# Check if next token is a dot
# Check if this is a function call (template call)
if self.current() != nil && self.current().type == animation_dsl.Token.LEFT_PAREN
# This is a standalone function call - check if it's a template
if self.template_definitions.contains(object_name)
var args = self.process_function_arguments()
var full_args = args != "" ? f"engine, {args}" : "engine"
var inline_comment = self.collect_inline_comment()
self.add(f"{object_name}_template({full_args}){inline_comment}")
# Track that we have template calls to trigger engine.start()
self.has_template_calls = true
else
self.error(f"Standalone function calls are only supported for templates. '{object_name}' is not a template.")
self.skip_statement()
end
return
end
# Check if next token is a dot (property assignment)
if self.current() != nil && self.current().type == animation_dsl.Token.DOT
self.next() # skip '.'
var property_name = self.expect_identifier()
@ -1097,6 +1116,11 @@ class SimpleDSLTranspiler
self.next() # consume '.'
var property_name = self.expect_identifier()
# Special handling for user.function_name() calls
if name == "user"
return self._process_user_function_call(property_name)
end
# Validate that the property exists on the referenced object
if self.symbol_table.contains(name)
var instance = self.symbol_table[name]
@ -1222,7 +1246,6 @@ class SimpleDSLTranspiler
# We're permissive here - any expression with these patterns gets a closure
var has_dynamic_content = (
string.find(left, "(") >= 0 || string.find(right, "(") >= 0 || # Function calls
string.find(left, "animation.global") >= 0 || string.find(right, "animation.global") >= 0 || # Variable refs
string.find(left, "animation.") >= 0 || string.find(right, "animation.") >= 0 || # Animation module calls
string.find(left, "_") >= 0 || string.find(right, "_") >= 0 # User variables (might be ValueProviders)
)
@ -1468,10 +1491,11 @@ class SimpleDSLTranspiler
var args = self.process_function_arguments()
# Check if it's a user-defined function first
if animation.is_user_function(func_name)
# Check if it's a template call first
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
var full_args = args != "" ? f"engine, {args}" : "engine"
return f"animation.get_user_function('{func_name}')({full_args})"
return f"{func_name}_template({full_args})"
else
# All functions are resolved from the animation module and need engine as first parameter
if args != ""
@ -1787,11 +1811,12 @@ class SimpleDSLTranspiler
return f"self.{func_name}({args})"
end
# Check if this is a user-defined function
if animation.is_user_function(func_name)
# Check if this is a template call
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
var args = self.process_function_arguments_for_expression()
var full_args = args != "" ? f"self.engine, {args}" : "self.engine"
return f"animation.get_user_function('{func_name}')({full_args})"
return f"{func_name}_template({full_args})"
end
# For other functions, this shouldn't happen in expression context
@ -1839,6 +1864,11 @@ class SimpleDSLTranspiler
self.next() # consume '.'
var property_name = self.expect_identifier()
# Special handling for user.function_name() calls
if name == "user"
return self._process_user_function_call(property_name)
end
# Validate that the property exists on the referenced object
if self.symbol_table.contains(name)
var instance = self.symbol_table[name]
@ -1897,13 +1927,12 @@ class SimpleDSLTranspiler
return f"self.{func_name}({args})" # Prefix with self. for closure context
end
# Check if this is a user-defined function
if animation.is_user_function(func_name)
# User functions use positional parameters with engine as first argument
# In closure context, use self.engine to access the engine from the ClosureValueProvider
# Check if this is a template call
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
var args = self.process_function_arguments_for_expression()
var full_args = args != "" ? f"self.engine, {args}" : "self.engine"
return f"animation.get_user_function('{func_name}')({full_args})"
return f"{func_name}_template({full_args})"
else
# Check if this is a simple function call without named parameters
if self._is_simple_function_call(func_name)
@ -2343,8 +2372,8 @@ class SimpleDSLTranspiler
# Generate single engine.start() call for all run statements
def generate_engine_start()
if size(self.run_statements) == 0
return # No run statements, no need to start engine
if size(self.run_statements) == 0 && !self.has_template_calls
return # No run statements or template calls, no need to start engine
end
# Add all animations/sequences to the engine
@ -2445,6 +2474,55 @@ class SimpleDSLTranspiler
self.strip_initialized = true
end
# Generate Berry function for template definition
def generate_template_function(name, params, param_types, body_tokens)
import string
# Generate function signature with engine as first parameter
var param_list = "engine"
for param : params
param_list += f", {param}_"
end
self.add(f"# Template function: {name}")
self.add(f"def {name}_template({param_list})")
# Create a new transpiler instance for the template body
var template_transpiler = animation_dsl.SimpleDSLTranspiler(body_tokens)
template_transpiler.symbol_table = {} # Fresh symbol table for template
template_transpiler.strip_initialized = true # Templates assume engine exists
# Add parameters to template's symbol table
for param : params
template_transpiler.symbol_table[param] = "parameter"
end
# Transpile the template body
var template_body = template_transpiler.transpile_template_body()
if template_body != nil
# Add the transpiled body with proper indentation
var body_lines = string.split(template_body, "\n")
for line : body_lines
if size(line) > 0
self.add(f" {line}") # Add 2-space indentation
end
end
else
# Error in template body transpilation
for error : template_transpiler.errors
self.error(f"Template '{name}' body error: {error}")
end
end
self.add("end")
self.add("")
# Register the template as a user function
self.add(f"animation.register_user_function('{name}', {name}_template)")
self.add("")
end
# Process named arguments for animation declarations with parameter validation
#
# @param var_name: string - Variable name to assign parameters to

View File

@ -10,7 +10,8 @@
#@ solidify:RichPaletteColorProvider,weak
class RichPaletteColorProvider : animation.color_provider
# Non-parameter instance variables only
var slots_arr # Constructed array of timestamp slots
var slots_arr # Constructed array of timestamp slots, based on cycle_period
var value_arr # Constructed array of value slots, based on range_min/range_max
var slots # Number of slots in the palette
var current_color # Current interpolated color (calculated during update)
var light_state # light_state instance for proper color calculations
@ -23,7 +24,7 @@ class RichPaletteColorProvider : animation.color_provider
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.SINE},
"brightness": {"min": 0, "max": 255, "default": 255},
"range_min": {"default": 0},
"range_max": {"default": 100}
"range_max": {"default": 255}
}
# Initialize a new RichPaletteColorProvider
@ -35,7 +36,6 @@ class RichPaletteColorProvider : animation.color_provider
# Initialize non-parameter instance variables
self.current_color = 0xFFFFFFFF
self.cycle_start = self.engine.time_ms # Initialize cycle start time
self.slots_arr = nil
self.slots = 0
# Create light_state instance for proper color calculations (reuse from Animate_palette)
@ -48,30 +48,10 @@ class RichPaletteColorProvider : animation.color_provider
# @param name: string - Name of the parameter that changed
# @param value: any - New value of the parameter
def on_param_changed(name, value)
if name == "palette"
# When palette changes, recompute slots
self._recompute_palette()
elif name == "cycle_period"
# When cycle_period changes, recompute the palette slots array
if value == nil return end
if value < 0 raise "value_error", "cycle_period must be non-negative" end
# Recompute palette with new cycle period (only if > 0 for time-based cycling)
if value > 0 && self._get_palette_bytes() != nil
self.slots_arr = self._parse_palette(0, value - 1)
end
elif name == "range_min" || name == "range_max"
# When range changes, recompute the palette slots array
var range_min = self.range_min
var range_max = self.range_max
if (range_min != nil) && (range_max != nil)
if range_min >= range_max raise "value_error", "range_min must be lower than range_max" end
# Recompute palette with new range
if self._get_palette_bytes() != nil
self.slots_arr = self._parse_palette(range_min, range_max)
end
if name == "range_min" || name == "range_max" || name == "cycle_period" || name == "palette"
if (self.slots_arr != nil) || (self.value_arr != nil)
# only if they were already computed
self._recompute_palette()
end
end
end
@ -81,10 +61,11 @@ class RichPaletteColorProvider : animation.color_provider
# @param time_ms: int - Time in milliseconds to set as cycle start (optional, uses engine time if nil)
# @return self for method chaining
def start(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
# Compute arrays if they were not yet initialized
if (self.slots_arr == nil) && (self.value_arr == nil)
self._recompute_palette()
end
self.cycle_start = time_ms
self.cycle_start = (time_ms != nil) ? time_ms : self.engine.time_ms
return self
end
@ -106,18 +87,27 @@ class RichPaletteColorProvider : animation.color_provider
# Recompute palette slots and metadata
def _recompute_palette()
# Compute slots_arr based on 'cycle_period'
var cycle_period = self.cycle_period
var palette_bytes = self._get_palette_bytes()
self.slots = size(palette_bytes) / 4
# Recompute palette (from Animate_palette)
var cycle_period = self.cycle_period
# Recompute palette with new cycle period (only if > 0 for time-based cycling)
if cycle_period > 0 && palette_bytes != nil
self.slots_arr = self._parse_palette(0, cycle_period - 1)
else
self.slots_arr = nil
end
# Compute value_arr based on 'range_min' and 'range_max'
var range_min = self.range_min
var range_max = self.range_max
if cycle_period != nil && cycle_period > 0
self.slots_arr = self._parse_palette(0, cycle_period - 1)
elif (range_min != nil) && (range_max != nil)
self.slots_arr = self._parse_palette(range_min, range_max)
if range_min >= range_max raise "value_error", "range_min must be lower than range_max" end
# Recompute palette with new range
if self._get_palette_bytes() != nil
self.value_arr = self._parse_palette(range_min, range_max)
else
self.value_arr = nil
end
# Set initial color
@ -188,6 +178,9 @@ class RichPaletteColorProvider : animation.color_provider
# @param time_ms: int - Current time in milliseconds
# @return int - Color in ARGB format (0xAARRGGBB)
def produce_value(name, time_ms)
if (self.slots_arr == nil) && (self.value_arr == nil)
self._recompute_palette()
end
var palette_bytes = self._get_palette_bytes()
if palette_bytes == nil || self.slots < 2
@ -267,26 +260,15 @@ class RichPaletteColorProvider : animation.color_provider
return final_color
end
# Set the range for value mapping (reused from Animate_palette)
#
# @param min: int - Minimum value for the range
# @param max: int - Maximum value for the range
# @return self for method chaining
def set_range(min, max)
if min >= max raise "value_error", "min must be lower than max" end
self.range_min = min
self.range_max = max
self.slots_arr = self._parse_palette(min, max)
return self
end
# Get color for a specific value (reused from Animate_palette.set_value)
#
# @param value: int/float - Value to map to a color
# @param time_ms: int - Current time in milliseconds (ignored for value-based color)
# @return int - Color in ARGB format
def get_color_for_value(value, time_ms)
if (self.slots_arr == nil) && (self.value_arr == nil)
self._recompute_palette()
end
var palette_bytes = self._get_palette_bytes()
var range_min = self.range_min
@ -299,14 +281,14 @@ class RichPaletteColorProvider : animation.color_provider
var slots = self.slots
var idx = slots - 2
while idx > 0
if value >= self.slots_arr[idx] break end
if value >= self.value_arr[idx] break end
idx -= 1
end
var bgrt0 = palette_bytes.get(idx * 4, 4)
var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)
var t0 = self.slots_arr[idx]
var t1 = self.slots_arr[idx + 1]
var t0 = self.value_arr[idx]
var t1 = self.value_arr[idx + 1]
# Use tasmota.scale_uint for efficiency (from Animate_palette)
var r = tasmota.scale_uint(value, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)

View File

@ -0,0 +1,352 @@
# Animation Engine Test Suite
# Comprehensive tests for the unified AnimationEngine
import animation
print("=== Animation Engine Opcaity Test Suite ===")
# Test utilities
var test_count = 0
var passed_count = 0
def assert_test(condition, message)
test_count += 1
if condition
passed_count += 1
print(f"✓ PASS: {message}")
else
print(f"✗ FAIL: {message}")
end
end
def assert_equals(actual, expected, message)
assert_test(actual == expected, f"{message} (expected: {expected}, actual: {actual})")
end
def assert_not_nil(value, message)
assert_test(value != nil, f"{message} (value was nil)")
end
# Test 11: Animation Opacity with Animation Masks
print("\n--- Test 11: Animation Opacity with Animation Masks ---")
# Create a fresh engine for opacity tests
var opacity_strip = global.Leds(10)
var opacity_engine = animation.animation_engine(opacity_strip)
# Test 11a: Basic numeric opacity
print("\n--- Test 11a: Basic numeric opacity ---")
var base_anim = animation.solid(opacity_engine)
base_anim.color = 0xFFFF0000 # Red
base_anim.opacity = 128 # 50% opacity
base_anim.priority = 10
base_anim.name = "base_red"
opacity_engine.add_animation(base_anim)
opacity_engine.start()
# Create frame buffer and test rendering
var opacity_frame = animation.frame_buffer(10)
base_anim.start()
var render_result = base_anim.render(opacity_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation with numeric opacity should render successfully")
assert_equals(base_anim.opacity, 128, "Numeric opacity should be preserved")
# Test 11b: Animation as opacity mask - basic setup
print("\n--- Test 11b: Animation as opacity mask - basic setup ---")
# Create opacity mask animation (pulsing from 0 to 255)
var opacity_mask = animation.solid(opacity_engine)
opacity_mask.color = 0xFF808080 # Gray (128 brightness)
opacity_mask.priority = 5
opacity_mask.name = "opacity_mask"
# Create main animation with animation opacity
var masked_anim = animation.solid(opacity_engine)
masked_anim.color = 0xFF00FF00 # Green
masked_anim.opacity = opacity_mask # Use animation as opacity
masked_anim.priority = 15
masked_anim.name = "masked_green"
assert_test(isinstance(masked_anim.opacity, animation.animation), "Opacity should be an animation instance")
assert_equals(masked_anim.opacity.name, "opacity_mask", "Opacity animation should be correctly assigned")
# Test 11c: Animation opacity rendering
print("\n--- Test 11c: Animation opacity rendering ---")
opacity_engine.clear()
opacity_engine.add_animation(masked_anim)
opacity_engine.start()
# Start both animations
masked_anim.start()
opacity_mask.start()
# Test rendering with animation opacity
var masked_frame = animation.frame_buffer(10)
render_result = masked_anim.render(masked_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation with animation opacity should render successfully")
assert_not_nil(masked_anim.opacity_frame, "Opacity frame buffer should be created")
assert_equals(masked_anim.opacity_frame.width, 10, "Opacity frame buffer should match main frame width")
# Test 11e: Complex opacity animation scenarios
print("\n--- Test 11e: Complex opacity animation scenarios ---")
# Create a pulsing opacity mask
var pulsing_opacity = animation.solid(opacity_engine)
pulsing_opacity.color = 0xFF000000 # Start with black (0 opacity)
pulsing_opacity.priority = 1
pulsing_opacity.name = "pulsing_opacity"
# Create animated color base
var rainbow_base = animation.solid(opacity_engine)
rainbow_base.color = 0xFFFF0000 # Red base
rainbow_base.opacity = pulsing_opacity # Pulsing opacity
rainbow_base.priority = 20
rainbow_base.name = "rainbow_with_pulse"
# Test multiple renders with changing opacity
opacity_engine.clear()
opacity_engine.add_animation(rainbow_base)
rainbow_base.start()
pulsing_opacity.start()
# Test simple opacity changes
var test_frame = animation.frame_buffer(10)
var base_time = 2000
# Update opacity animation color to simulate pulsing
pulsing_opacity.color = 0xFF808080 # Gray (50% opacity)
render_result = rainbow_base.render(test_frame, base_time)
assert_test(render_result, "Complex opacity animation should render successfully")
# Test 11f: Opacity animation lifecycle management
print("\n--- Test 11f: Opacity animation lifecycle management ---")
# Test that opacity animation starts automatically when main animation renders
var auto_start_opacity = animation.solid(opacity_engine)
auto_start_opacity.color = 0xFF808080 # Gray
auto_start_opacity.priority = 1
auto_start_opacity.name = "auto_start_opacity"
auto_start_opacity.is_running = false # Start stopped
var auto_start_main = animation.solid(opacity_engine)
auto_start_main.color = 0xFFFFFF00 # Yellow
auto_start_main.opacity = auto_start_opacity
auto_start_main.priority = 10
auto_start_main.name = "auto_start_main"
# Opacity animation should not be running initially
assert_test(!auto_start_opacity.is_running, "Opacity animation should start stopped")
# Start main animation and render
auto_start_main.start()
var auto_frame = animation.frame_buffer(10)
render_result = auto_start_main.render(auto_frame, opacity_engine.time_ms)
# Opacity animation should now be running
assert_test(auto_start_opacity.is_running, "Opacity animation should auto-start when main animation renders")
assert_test(render_result, "Main animation with auto-started opacity should render successfully")
# Test 11g: Nested animation opacity (animation with animation opacity)
print("\n--- Test 11g: Nested animation opacity ---")
# Create a chain: base -> opacity1 -> opacity2
var base_nested = animation.solid(opacity_engine)
base_nested.color = 0xFF00FFFF # Cyan
base_nested.priority = 30
base_nested.name = "base_nested"
var opacity1 = animation.solid(opacity_engine)
opacity1.color = 0xFF808080 # 50% gray
opacity1.priority = 25
opacity1.name = "opacity1"
var opacity2 = animation.solid(opacity_engine)
opacity2.color = 0xFFC0C0C0 # 75% gray
opacity2.priority = 20
opacity2.name = "opacity2"
# Chain the opacities: base uses opacity1, opacity1 uses opacity2
opacity1.opacity = opacity2
base_nested.opacity = opacity1
# Test rendering with nested opacity
opacity_engine.clear()
opacity_engine.add_animation(base_nested)
base_nested.start()
opacity1.start()
opacity2.start()
var nested_frame = animation.frame_buffer(10)
render_result = base_nested.render(nested_frame, opacity_engine.time_ms)
assert_test(render_result, "Nested animation opacity should render successfully")
assert_not_nil(base_nested.opacity_frame, "Base animation should have opacity frame buffer")
assert_not_nil(opacity1.opacity_frame, "First opacity animation should have opacity frame buffer")
# Test 11h: Opacity animation parameter changes
print("\n--- Test 11h: Opacity animation parameter changes ---")
var param_base = animation.solid(opacity_engine)
param_base.color = 0xFFFF00FF # Magenta
param_base.priority = 10
param_base.name = "param_base"
var param_opacity = animation.solid(opacity_engine)
param_opacity.color = 0xFF404040 # Dark gray
param_opacity.priority = 5
param_opacity.name = "param_opacity"
param_base.opacity = param_opacity
# Test changing opacity animation parameters
param_base.start()
param_opacity.start()
var param_frame = animation.frame_buffer(10)
render_result = param_base.render(param_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation should render before opacity parameter change")
# Change opacity animation color
param_opacity.color = 0xFFFFFFFF # White (full opacity)
render_result = param_base.render(param_frame, opacity_engine.time_ms + 100)
assert_test(render_result, "Animation should render after opacity parameter change")
# Change opacity animation to numeric value
param_base.opacity = 64 # 25% opacity
render_result = param_base.render(param_frame, opacity_engine.time_ms + 200)
assert_test(render_result, "Animation should render after changing from animation to numeric opacity")
# Change back to animation opacity
param_base.opacity = param_opacity
render_result = param_base.render(param_frame, opacity_engine.time_ms + 300)
assert_test(render_result, "Animation should render after changing from numeric to animation opacity")
# Test 11i: Opacity with full transparency and full opacity
print("\n--- Test 11i: Opacity edge cases ---")
var edge_base = animation.solid(opacity_engine)
edge_base.color = 0xFF0080FF # Blue
edge_base.priority = 10
edge_base.name = "edge_base"
# Test full transparency (should still render but with no visible effect)
edge_base.opacity = 0
edge_base.start()
var edge_frame = animation.frame_buffer(10)
render_result = edge_base.render(edge_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation with 0 opacity should still render")
# Test full opacity (should render normally)
edge_base.opacity = 255
render_result = edge_base.render(edge_frame, opacity_engine.time_ms + 100)
assert_test(render_result, "Animation with full opacity should render normally")
# Test transparent animation as opacity
var transparent_opacity = animation.solid(opacity_engine)
transparent_opacity.color = 0x00000000 # Fully transparent
transparent_opacity.priority = 5
transparent_opacity.name = "transparent_opacity"
edge_base.opacity = transparent_opacity
transparent_opacity.start()
render_result = edge_base.render(edge_frame, opacity_engine.time_ms + 200)
assert_test(render_result, "Animation with transparent animation opacity should render")
# Test 11j: Performance with animation opacity
print("\n--- Test 11j: Performance with animation opacity ---")
# Create multiple animations with animation opacity for performance testing
opacity_engine.clear()
var perf_animations = []
var perf_opacities = []
for i : 0..9
var perf_base = animation.solid(opacity_engine)
perf_base.color = 0xFF000000 | ((i * 25) << 16) | ((i * 15) << 8) | (i * 10)
perf_base.priority = 50 + i
perf_base.name = f"perf_base_{i}"
var perf_opacity = animation.solid(opacity_engine)
perf_opacity.color = 0xFF808080 # 50% gray
perf_opacity.priority = 40 + i
perf_opacity.name = f"perf_opacity_{i}"
perf_base.opacity = perf_opacity
perf_animations.push(perf_base)
perf_opacities.push(perf_opacity)
opacity_engine.add_animation(perf_base)
end
# Start all animations
for anim : perf_animations
anim.start()
end
for opacity : perf_opacities
opacity.start()
end
# Performance test: render multiple times
var perf_start_time = tasmota.millis()
for i : 0..19
opacity_engine.on_tick(perf_start_time + i * 10)
end
var perf_time = tasmota.millis() - perf_start_time
assert_test(perf_time < 300, f"20 render cycles with 10 animation opacities should be reasonable (took {perf_time}ms)")
assert_equals(opacity_engine.size(), 10, "Should have 10 animations with animation opacity")
# Verify all opacity frame buffers were created
var opacity_frames_created = 0
for anim : perf_animations
if anim.opacity_frame != nil
opacity_frames_created += 1
end
end
assert_test(opacity_frames_created >= 5, f"Most animations should have opacity frame buffers created (found {opacity_frames_created})")
opacity_engine.stop()
# Test Results
print(f"\n=== Test Results ===")
print(f"Tests run: {test_count}")
print(f"Tests passed: {passed_count}")
print(f"Tests failed: {test_count - passed_count}")
print(f"Success rate: {tasmota.scale_uint(passed_count, 0, test_count, 0, 100)}%")
if passed_count == test_count
print("🎉 All tests passed!")
else
print("❌ Some tests failed")
raise "test_failed"
end
print("\n=== Animation Opacity Features ===")
print("Animation opacity system supports:")
print("- Numeric opacity values (0-255)")
print("- Animation instances as opacity masks")
print("- Automatic opacity animation lifecycle management")
print("- Efficient opacity frame buffer reuse")
print("- Dynamic frame buffer resizing")
print("- Nested animation opacity chains")
print("- Real-time opacity parameter changes")
print("- High performance with multiple opacity animations")
print("\n=== Performance Benefits ===")
print("Unified AnimationEngine benefits:")
print("- Single object replacing 3 separate classes")
print("- Reduced memory overhead and allocations")
print("- Simplified API surface")
print("- Better cache locality")
print("- Fewer method calls per frame")
print("- Cleaner codebase with no deprecated APIs")
print("- Maintained full functionality")

View File

@ -38,7 +38,7 @@ assert(anim.color == 0xFF0000, "Animation color should be red")
var default_anim = animation.animation(engine)
assert(default_anim.priority == 10, "Default priority should be 10")
assert(default_anim.duration == 0, "Default duration should be 0 (infinite)")
assert(default_anim.loop == true, "Default loop should be true")
assert(default_anim.loop == false, "Default loop should be false")
assert(default_anim.opacity == 255, "Default opacity should be 255")
assert(default_anim.name == "animation", "Default name should be 'animation'")
assert(default_anim.color == 0xFFFFFFFF, "Default color should be white")
@ -231,8 +231,6 @@ var valid_color_result = param_anim.set_param("color", 0xFF0000FF) # Valid
var invalid_opacity_result = param_anim.set_param("opacity", 300) # Invalid: above max (255)
assert(valid_priority_result == true, "Valid priority parameter should succeed")
assert(valid_color_result == true, "Valid color parameter should succeed")
assert(invalid_opacity_result == false, "Invalid opacity parameter should fail")
assert(param_anim.get_param("opacity", nil) == 128, "Invalid parameters should not change existing value")
# Test render method (base implementation should do nothing)
# Create a frame buffer for testing

View File

@ -116,7 +116,8 @@ fire_palette.palette = animation.PALETTE_FIRE
fire_palette.cycle_period = 5000
fire_palette.transition_type = 1 # Use sine transition (smooth)
fire_palette.brightness = 255
fire_palette.set_range(0, 255)
fire_palette.range_min = 0
fire_palette.range_max = 255
fire.color = fire_palette
print("Set back to fire palette")

View File

@ -1,6 +1,6 @@
# Test for global variable access with new transpiler symbol resolution
# Verifies that generated code uses animation.symbol for animation module symbols
# and symbol_ for user-defined variables (no more animation.global calls)
# and symbol_ for user-defined variables calls)
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation -e "import tasmota" lib/libesp32/berry_animation/tests/global_variable_test.be
@ -26,7 +26,7 @@ def test_global_variable_access()
assert(string.find(berry_code, "var red_alt_ = 0xFFFF0100") >= 0, "Should define red_alt variable")
assert(string.find(berry_code, "var solid_red_ = animation.solid(engine)") >= 0, "Should define solid_red variable with new pattern")
# Variable references should now use direct underscore notation (no animation.global)
# Variable references should now use direct underscore notation
assert(string.find(berry_code, "solid_red_.color = red_alt_") >= 0, "Should use red_alt_ directly for variable reference")
# Verify the generated code actually compiles by executing it
@ -50,7 +50,7 @@ def test_undefined_variable_exception()
assert(berry_code != nil, "Should compile DSL code")
# Check that undefined variables use direct underscore notation (no animation.global)
# Check that undefined variables use direct underscore notation
import string
assert(string.find(berry_code, "test_.color = undefined_var_") >= 0, "Should use undefined_var_ directly for undefined variable")

View File

@ -34,8 +34,8 @@ var pattern_anim = animation.palette_pattern_animation(mock_engine)
# Create a simple mock color source that has get_color_for_value method
class MockColorSource
def get_color_for_value(value, time_ms)
# Return red for high values, blue for low values
return value > 50 ? 0xFF0000FF : 0x0000FFFF
# Return red for high values, blue for low values (expecting 0-255 range)
return value > 127 ? 0xFF0000FF : 0x0000FFFF
end
end
var mock_color_source = MockColorSource()
@ -47,9 +47,9 @@ pattern_anim.loop = false
pattern_anim.opacity = 255
pattern_anim.name = "pattern_test"
# Create a simple pattern function that alternates between 0 and 100
# Create a simple pattern function that alternates between 0 and 255
def simple_pattern(pixel_index, time_ms, animation)
return pixel_index % 2 == 0 ? 100 : 0
return pixel_index % 2 == 0 ? 255 : 0
end
pattern_anim.pattern_func = simple_pattern
@ -126,6 +126,17 @@ assert(result, "Render should return true")
gradient_anim.shift_period = 1500
assert(gradient_anim.shift_period == 1500, "Shift period should be updated to 1500")
# Test new parameters
gradient_anim.spatial_period = 5
assert(gradient_anim.spatial_period == 5, "Spatial period should be updated to 5")
gradient_anim.phase_shift = 25
assert(gradient_anim.phase_shift == 25, "Phase shift should be updated to 25")
# Test static gradient (shift_period = 0)
gradient_anim.shift_period = 0
assert(gradient_anim.shift_period == 0, "Shift period should be updated to 0 (static)")
# Test 4: PaletteMeterAnimation
print("Test 4: PaletteMeterAnimation")
var meter_anim = animation.palette_meter_animation(mock_engine)
@ -133,7 +144,7 @@ meter_anim.color_source = mock_color_source
# Create a value function that returns 50% (half the strip)
def meter_value_func(time_ms, animation)
return 50 # 50% of the strip
return 50 # 50% of the strip (this is still 0-100 for meter logic)
end
meter_anim.value_func = meter_value_func
@ -157,7 +168,7 @@ assert(result, "Render should return true")
# Test changing value function
def new_meter_value_func(time_ms, animation)
return 75 # 75% of the strip
return 75 # 75% of the strip (this is still 0-100 for meter logic)
end
meter_anim.value_func = new_meter_value_func
@ -229,10 +240,10 @@ end
print("Test 7: Animation with different color mapping")
class MockRainbowColorSource
def get_color_for_value(value, time_ms)
# Simple rainbow mapping based on value
if value < 33
# Simple rainbow mapping based on value (expecting 0-255 range)
if value < 85
return 0xFF0000FF # Red
elif value < 66
elif value < 170
return 0x00FF00FF # Green
else
return 0x0000FFFF # Blue

View File

@ -39,11 +39,6 @@ def test_parameter_accepts_value_providers()
oscillator.form = animation.SAWTOOTH
assert(test_anim.set_param("opacity", oscillator) == true, "Should accept OscillatorValueProvider")
# Test that invalid types are rejected (no type conversion)
assert(test_anim.set_param("opacity", "invalid") == false, "Should reject string")
assert(test_anim.set_param("opacity", true) == false, "Should reject boolean")
assert(test_anim.set_param("opacity", 3.14) == true, "Should accept real (recent change to accept real for int parameters)")
print("✓ Parameter validation with ValueProviders test passed")
end
@ -91,11 +86,7 @@ def test_range_validation()
assert(test_anim.set_param("opacity", 50) == true, "Should accept value within range")
assert(test_anim.set_param("opacity", 0) == true, "Should accept minimum value")
assert(test_anim.set_param("opacity", 255) == true, "Should accept maximum value")
# Test invalid range values
assert(test_anim.set_param("opacity", -1) == false, "Should reject value below minimum")
assert(test_anim.set_param("opacity", 256) == false, "Should reject value above maximum")
print("✓ Range validation test passed")
end
@ -114,8 +105,6 @@ def test_range_validation_with_providers()
assert(test_anim.set_param("opacity", 50) == true, "Should accept value within range")
assert(test_anim.set_param("opacity", 0) == true, "Should accept minimum value")
assert(test_anim.set_param("opacity", 255) == true, "Should accept maximum value")
assert(test_anim.set_param("opacity", -1) == false, "Should reject value below minimum")
assert(test_anim.set_param("opacity", 256) == false, "Should reject value above maximum")
# Test that ValueProviders bypass range validation
# (since they provide dynamic values that can't be validated at set time)

View File

@ -125,10 +125,6 @@ print("Created static animation (cycle_period = 0)")
var css_gradient = anim.color_provider.to_css_gradient()
print(f"CSS gradient available: {bool(css_gradient)}")
anim.color_provider.set_range(0, 255)
var value_color = anim.color_provider.get_color_for_value(128, engine.time_ms)
print(f"Value-based color available: {bool(value_color)}")
# Validate key test results
assert(anim != nil, "Rich palette animation should be created")
assert(type(anim) == "instance", "Animation should be an instance")

View File

@ -189,11 +189,14 @@ class RichPaletteAnimationTest
self.assert_equal(provider.cycle_period, 1000, "Cycle period is 1000ms")
# Test range setting and value-based colors
provider.set_range(0, 100)
provider.range_min = 0
provider.range_max = 100
self.assert_equal(provider.range_min, 0, "Range min is 0")
self.assert_equal(provider.range_max, 100, "Range max is 100")
# Test value-based color generation
provider.start()
print(f"{provider.slots_arr=} {provider.value_arr=}")
var color_0 = provider.get_color_for_value(0, 0)
var color_50 = provider.get_color_for_value(50, 0)
var color_100 = provider.get_color_for_value(100, 0)
@ -233,7 +236,9 @@ class RichPaletteAnimationTest
var provider = animation.rich_palette(mock_engine)
provider.palette = palette
provider.cycle_period = 0 # Value-based mode
provider.set_range(0, 255)
provider.range_min = 0
provider.range_max = 255
provider.start()
# Check that cycle_period can be set to 0
self.assert_equal(provider.cycle_period, 0, "Cycle period can be set to 0")

View File

@ -1,6 +1,5 @@
# Symbol Registry Test Suite
# Tests for the simplified transpiler's runtime symbol resolution approach
# The simplified transpiler uses runtime resolution with new animation.global(name, module_name) signature
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation -e "import tasmota" lib/libesp32/berry_animation/tests/symbol_registry_test.be

View File

@ -49,6 +49,7 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/bytes_type_test.be", # Tests bytes type validation in parameterized objects
"lib/libesp32/berry_animation/src/tests/animation_test.be",
"lib/libesp32/berry_animation/src/tests/animation_engine_test.be",
"lib/libesp32/berry_animation/src/tests/animation_opacity_test.be",
"lib/libesp32/berry_animation/src/tests/fast_loop_integration_test.be",
"lib/libesp32/berry_animation/src/tests/solid_animation_test.be", # Tests unified solid() function
"lib/libesp32/berry_animation/src/tests/solid_unification_test.be", # Tests solid unification

View File

@ -4,6 +4,7 @@
import animation
import animation_dsl
import user_functions
# Load user functions
import "user_functions" as user_funcs
@ -32,7 +33,7 @@ def test_user_function_in_computed_parameters()
var dsl_code =
"animation random_base = solid(color=blue, priority=10)\n"
"random_base.opacity = rand_demo()\n"
"random_base.opacity = user.rand_demo()\n"
"run random_base"
try
@ -56,7 +57,7 @@ def test_user_function_with_math()
var dsl_code =
"animation random_bounded = solid(color=orange, priority=8)\n"
"random_bounded.opacity = max(50, min(255, rand_demo() + 100))\n"
"random_bounded.opacity = max(50, min(255, user.rand_demo() + 100))\n"
"run random_bounded"
try
@ -82,7 +83,7 @@ def test_user_function_in_arithmetic()
var dsl_code =
"animation random_variation = solid(color=purple, priority=15)\n"
"random_variation.opacity = abs(rand_demo() - 128) + 64\n"
"random_variation.opacity = abs(user.rand_demo() - 128) + 64\n"
"run random_variation"
try
@ -108,7 +109,7 @@ def test_complex_user_function_expressions()
var dsl_code =
"animation random_complex = solid(color=white, priority=20)\n"
"random_complex.opacity = round((rand_demo() + 128) / 2 + abs(rand_demo() - 100))\n"
"random_complex.opacity = round((user.rand_demo() + 128) / 2 + abs(user.rand_demo() - 100))\n"
"run random_complex"
try
@ -139,8 +140,8 @@ def test_generated_code_validity()
var dsl_code =
"animation random_multi = solid(color=cyan, priority=12)\n"
"random_multi.opacity = rand_demo()\n"
"random_multi.duration = max(100, rand_demo())\n"
"random_multi.opacity = user.rand_demo()\n"
"random_multi.duration = max(100, user.rand_demo())\n"
"run random_multi"
try