Berry animation templates (#24048)
This commit is contained in:
parent
f839a97fbb
commit
05979574d9
@ -0,0 +1,67 @@
|
||||
# Template animation for Cylon like eye
|
||||
|
||||
template animation shutter_bidir {
|
||||
param colors type palette
|
||||
param period type time default 5s
|
||||
param acending type bool default true
|
||||
|
||||
set strip_len = strip_length()
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||
|
||||
color col1 = color_cycle(palette=colors, cycle_period=0)
|
||||
color col2 = color_cycle(palette=colors, cycle_period=0)
|
||||
col2.next = 1
|
||||
|
||||
# shutter moving from left to right
|
||||
animation shutter_lr_animation = beacon_animation(
|
||||
color = col2
|
||||
back_color = col1
|
||||
pos = 0
|
||||
beacon_size = shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
# shutter moving from right to left
|
||||
animation shutter_rl_animation = beacon_animation(
|
||||
color = col1
|
||||
back_color = col2
|
||||
pos = 0
|
||||
beacon_size = strip_len - shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence shutter_seq repeat forever {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_lr_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_rl_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
|
||||
run main
|
||||
@ -1,85 +0,0 @@
|
||||
# DSL Compilation Report
|
||||
|
||||
Generated: Sam 23 aoû 2025 10:30:34 CEST
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total files**: 24
|
||||
- **Successfully compiled**: 24
|
||||
- **Failed to compile**: 0
|
||||
- **Success rate**: 100%
|
||||
|
||||
## Successfully Compiled Files
|
||||
|
||||
- ✅ aurora_borealis.anim
|
||||
- ✅ breathing_colors.anim
|
||||
- ✅ candy_cane.anim
|
||||
- ✅ christmas_tree.anim
|
||||
- ✅ comet_chase.anim
|
||||
- ✅ disco_strobe.anim
|
||||
- ✅ fire_flicker.anim
|
||||
- ✅ heartbeat_pulse.anim
|
||||
- ✅ lava_lamp.anim
|
||||
- ✅ lightning_storm.anim
|
||||
- ✅ matrix_rain.anim
|
||||
- ✅ meteor_shower.anim
|
||||
- ✅ neon_glow.anim
|
||||
- ✅ ocean_waves.anim
|
||||
- ✅ palette_demo.anim
|
||||
- ✅ palette_showcase.anim
|
||||
- ✅ plasma_wave.anim
|
||||
- ✅ police_lights.anim
|
||||
- ✅ property_assignment_demo.anim
|
||||
- ✅ rainbow_cycle.anim
|
||||
- ✅ scanner_larson.anim
|
||||
- ✅ simple_palette.anim
|
||||
- ✅ sunrise_sunset.anim
|
||||
- ✅ twinkle_stars.anim
|
||||
|
||||
## Failed Compilations
|
||||
|
||||
|
||||
## Common Issues Found
|
||||
|
||||
Based on the compilation attempts, the following issues are common:
|
||||
|
||||
### 1. Comments in Palette Definitions
|
||||
Many files fail because comments are included within palette array definitions:
|
||||
```
|
||||
palette fire_colors = [
|
||||
(0, #000000), # This comment causes parsing errors
|
||||
(128, #FF0000) # This too
|
||||
]
|
||||
```
|
||||
|
||||
**Solution**: Remove comments from within palette definitions.
|
||||
|
||||
### 2. Comments in Function Arguments
|
||||
Comments within function calls break the parser:
|
||||
```
|
||||
animation pulse_red = pulse(
|
||||
solid(red),
|
||||
2s, # This comment breaks parsing
|
||||
20%, 100%
|
||||
)
|
||||
```
|
||||
|
||||
**Solution**: Remove comments from function argument lists.
|
||||
|
||||
### 3. Missing Function Parameters
|
||||
Some function calls expect specific parameter formats that aren't provided.
|
||||
|
||||
### 4. Property Assignments Not Supported
|
||||
Object property assignments like `stripe1.pos = 3` are not handled correctly.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Clean DSL Syntax**: Remove all inline comments from complex expressions
|
||||
2. **Full Parameter Lists**: Always provide complete parameter lists to functions
|
||||
3. **Use Sequences**: Instead of property assignments, use sequence-based approaches
|
||||
4. **Test Incrementally**: Start with simple examples and build complexity gradually
|
||||
|
||||
## Working Examples
|
||||
|
||||
The successfully compiled files can be used as templates for creating new DSL animations.
|
||||
|
||||
@ -0,0 +1,164 @@
|
||||
# Generated Berry code from Animation DSL
|
||||
# Source: chap_5_21_template_shutter_bidir.anim
|
||||
#
|
||||
# This file was automatically generated by compile_all_examples.sh
|
||||
# Do not edit manually - changes will be overwritten
|
||||
|
||||
import animation
|
||||
|
||||
# Template animation for Cylon like eye
|
||||
# Template animation class: shutter_bidir
|
||||
class shutter_bidir_animation : animation.engine_proxy
|
||||
static var PARAMS = animation.enc_params({
|
||||
"colors": {"type": "palette"},
|
||||
"period": {"type": "time", "default": 5000},
|
||||
"acending": {"type": "bool", "default": true}
|
||||
})
|
||||
|
||||
# Template setup method - overrides EngineProxy placeholder
|
||||
def setup_template()
|
||||
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
|
||||
|
||||
var strip_len_ = animation.strip_length(engine)
|
||||
var shutter_size_ = (def (engine)
|
||||
var provider = animation.sawtooth(engine)
|
||||
provider.min_value = 0
|
||||
provider.max_value = strip_len_
|
||||
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
|
||||
return provider
|
||||
end)(engine)
|
||||
var col1_ = animation.color_cycle(engine)
|
||||
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
|
||||
col1_.cycle_period = 0
|
||||
var col2_ = animation.color_cycle(engine)
|
||||
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
|
||||
col2_.cycle_period = 0
|
||||
col2_.next = 1
|
||||
# shutter moving from left to right
|
||||
var shutter_lr_animation_ = animation.beacon_animation(engine)
|
||||
shutter_lr_animation_.color = col2_
|
||||
shutter_lr_animation_.back_color = col1_
|
||||
shutter_lr_animation_.pos = 0
|
||||
shutter_lr_animation_.beacon_size = shutter_size_
|
||||
shutter_lr_animation_.slew_size = 0
|
||||
shutter_lr_animation_.priority = 5
|
||||
# shutter moving from right to left
|
||||
var shutter_rl_animation_ = animation.beacon_animation(engine)
|
||||
shutter_rl_animation_.color = col1_
|
||||
shutter_rl_animation_.back_color = col2_
|
||||
shutter_rl_animation_.pos = 0
|
||||
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
|
||||
shutter_rl_animation_.slew_size = 0
|
||||
shutter_rl_animation_.priority = 5
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_lr_animation_, def (engine) return self.period end)
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_rl_animation_, def (engine) return self.period end)
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
self.add(shutter_seq_)
|
||||
end
|
||||
end
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
# Auto-generated strip initialization (using Tasmota configuration)
|
||||
var engine = animation.init_strip()
|
||||
|
||||
var rainbow_with_white_ = bytes(
|
||||
"FFFC0000" # Red
|
||||
"FFFF8000" # Orange
|
||||
"FFFFFF00" # Yellow
|
||||
"FF00FF00" # Green
|
||||
"FF00FFFF" # Cyan
|
||||
"FF0080FF" # Blue
|
||||
"FF8000FF" # Violet
|
||||
"FFCCCCCC" # White
|
||||
"FFFC0000" # Red - need to add the first color at last position to ensure roll-over
|
||||
)
|
||||
var main_ = shutter_bidir_animation(engine)
|
||||
main_.colors = rainbow_with_white_
|
||||
main_.period = 1500
|
||||
engine.add(main_)
|
||||
engine.run()
|
||||
|
||||
# Compilation warnings:
|
||||
# Line 51: Template 'shutter_bidir' parameter 'acending' is declared but never used in the template body.
|
||||
|
||||
|
||||
#- Original DSL source:
|
||||
# Template animation for Cylon like eye
|
||||
|
||||
template animation shutter_bidir {
|
||||
param colors type palette
|
||||
param period type time default 5s
|
||||
param acending type bool default true
|
||||
|
||||
set strip_len = strip_length()
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||
|
||||
color col1 = color_cycle(palette=colors, cycle_period=0)
|
||||
color col2 = color_cycle(palette=colors, cycle_period=0)
|
||||
col2.next = 1
|
||||
|
||||
# shutter moving from left to right
|
||||
animation shutter_lr_animation = beacon_animation(
|
||||
color = col2
|
||||
back_color = col1
|
||||
pos = 0
|
||||
beacon_size = shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
# shutter moving from right to left
|
||||
animation shutter_rl_animation = beacon_animation(
|
||||
color = col1
|
||||
back_color = col2
|
||||
pos = 0
|
||||
beacon_size = strip_len - shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence shutter_seq repeat forever {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_lr_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_rl_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
|
||||
run main
|
||||
|
||||
-#
|
||||
@ -61,6 +61,24 @@ SUCCESS
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## chap_5_21_template_shutter_bidir.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
|
||||
## Symbol Table
|
||||
|
||||
| Symbol | Type | Builtin | Dangerous | Takes Args |
|
||||
|----------------------|-----------------------|---------|-----------|------------|
|
||||
| `main` | animation | | | |
|
||||
| `rainbow_with_white` | palette | | | |
|
||||
| `shutter_bidir` | animation_constructor | | | ✓ |
|
||||
|
||||
### Compilation Output
|
||||
|
||||
```
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## christmas_tree.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
@ -252,15 +270,8 @@ SUCCESS
|
||||
|
||||
| Symbol | Type | Builtin | Dangerous | Takes Args |
|
||||
|----------------------|----------|---------|-----------|------------|
|
||||
| `blue` | color | ✓ | | |
|
||||
| `green` | color | ✓ | | |
|
||||
| `indigo` | color | ✓ | | |
|
||||
| `orange` | color | ✓ | | |
|
||||
| `rainbow_with_white` | palette | | | |
|
||||
| `red` | color | ✓ | | |
|
||||
| `shutter_bidir` | template | | | |
|
||||
| `white` | color | ✓ | | |
|
||||
| `yellow` | color | ✓ | | |
|
||||
|
||||
### Compilation Output
|
||||
|
||||
@ -1086,6 +1097,27 @@ SUCCESS
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## test_template_animation.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
|
||||
## Symbol Table
|
||||
|
||||
| Symbol | Type | Builtin | Dangerous | Takes Args |
|
||||
|-------------------|-----------------------|---------|-----------|------------|
|
||||
| `blue` | color | ✓ | | |
|
||||
| `green` | color | ✓ | | |
|
||||
| `my_shutter` | animation | | | |
|
||||
| `rainbow_colors` | palette | | | |
|
||||
| `red` | color | ✓ | | |
|
||||
| `shutter_central` | animation_constructor | | | ✓ |
|
||||
|
||||
### Compilation Output
|
||||
|
||||
```
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## test_template_simple_reusable.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
@ -1177,14 +1209,15 @@ SUCCESS
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total files processed:** 47
|
||||
- **Successfully compiled:** 44
|
||||
- **Total files processed:** 49
|
||||
- **Successfully compiled:** 46
|
||||
- **Failed to compile:** 3
|
||||
|
||||
### Successful Files
|
||||
|
||||
- ✅ breathing_colors.anim
|
||||
- ✅ candy_cane.anim
|
||||
- ✅ chap_5_21_template_shutter_bidir.anim
|
||||
- ✅ christmas_tree.anim
|
||||
- ✅ comet_chase.anim
|
||||
- ✅ computed_values_demo.anim
|
||||
@ -1223,6 +1256,7 @@ SUCCESS
|
||||
- ✅ test_shutter_rainbow_bidir.anim
|
||||
- ✅ test_shutter_rainbow_central.anim
|
||||
- ✅ test_simple_math.anim
|
||||
- ✅ test_template_animation.anim
|
||||
- ✅ test_template_simple_reusable.anim
|
||||
- ✅ test_template_simple.anim
|
||||
- ✅ twinkle_stars.anim
|
||||
|
||||
@ -36,7 +36,7 @@ red_eye_.color = eye_color_ # palette that will advance when we do `eye_color.n
|
||||
red_eye_.pos = cosine_val_ # oscillator for position
|
||||
red_eye_.beacon_size = 3 # small 3 pixels eye
|
||||
red_eye_.slew_size = 2 # with 2 pixel shading around
|
||||
var cylon_eye_ = animation.SequenceManager(engine, -1)
|
||||
var cylon_eye_ = animation.sequence_manager(engine, -1)
|
||||
.push_closure_step(def (engine) cosine_val_.start(engine.time_ms) end)
|
||||
.push_play_step(red_eye_, animation.resolve(eye_duration_)) # use COSINE movement
|
||||
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end) # switch to TRIANGLE
|
||||
|
||||
@ -36,7 +36,7 @@ shutter_animation_.beacon_size = shutter_size_
|
||||
shutter_animation_.slew_size = 0
|
||||
shutter_animation_.priority = 5
|
||||
log(f"foobar", 3)
|
||||
var shutter_run_ = animation.SequenceManager(engine, -1)
|
||||
var shutter_run_ = animation.sequence_manager(engine, -1)
|
||||
.push_closure_step(def (engine) log(f"before", 3) end)
|
||||
.push_play_step(shutter_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) log(f"after", 3) end)
|
||||
|
||||
@ -42,14 +42,14 @@ def shutter_bidir_template(engine, colors_, duration_)
|
||||
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
|
||||
shutter_rl_animation_.slew_size = 0
|
||||
shutter_rl_animation_.priority = 5
|
||||
var shutter_seq_ = animation.SequenceManager(engine, -1)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_lr_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_rl_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
@ -64,13 +64,14 @@ animation.register_user_function('shutter_bidir', shutter_bidir_template)
|
||||
var engine = animation.init_strip()
|
||||
|
||||
var rainbow_with_white_ = bytes(
|
||||
"FFFF0000"
|
||||
"FFFFA500"
|
||||
"FFFFFF00"
|
||||
"FF008000" # comma left on-purpose to test transpiler
|
||||
"FF0000FF" # need for a lighter blue
|
||||
"FF4B0082"
|
||||
"FFFFFFFF"
|
||||
"FFFC0000" # Red
|
||||
"FFFF8000" # Orange
|
||||
"FFFFFF00" # Yellow
|
||||
"FF00FF00" # Green
|
||||
"FF00FFFF" # Cyan
|
||||
"FF0080FF" # Blue
|
||||
"FF8000FF" # Violet
|
||||
"FFCCCCCC" # White
|
||||
)
|
||||
shutter_bidir_template(engine, rainbow_with_white_, 1500)
|
||||
engine.run()
|
||||
@ -130,13 +131,15 @@ template shutter_bidir {
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
palette rainbow_with_white = [ red
|
||||
orange
|
||||
yellow
|
||||
green, # comma left on-purpose to test transpiler
|
||||
blue # need for a lighter blue
|
||||
indigo
|
||||
white
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
]
|
||||
|
||||
shutter_bidir(rainbow_with_white, 1.5s)
|
||||
|
||||
@ -43,14 +43,14 @@ def shutter_central_template(engine, colors_, duration_)
|
||||
shutter_outin_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
|
||||
shutter_outin_animation_.slew_size = 0
|
||||
shutter_outin_animation_.priority = 5
|
||||
var shutter_seq_ = animation.SequenceManager(engine, -1)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_inout_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_outin_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
|
||||
@ -34,7 +34,7 @@ def shutter_lr_template(engine, colors_, duration_)
|
||||
shutter_lr_animation_.beacon_size = shutter_size_
|
||||
shutter_lr_animation_.slew_size = 0
|
||||
shutter_lr_animation_.priority = 5
|
||||
var shutter_seq_ = animation.SequenceManager(engine, -1)
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_lr_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
|
||||
@ -24,7 +24,7 @@ var dynamic_green_ = animation.solid(engine)
|
||||
dynamic_green_.color = 0xFF008000
|
||||
dynamic_green_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.abs(animation.get_user_function('rand_demo')(engine) - 128) + 64 end)
|
||||
# Create a sequence that cycles through the animations
|
||||
var import_demo_ = animation.SequenceManager(engine)
|
||||
var import_demo_ = animation.sequence_manager(engine)
|
||||
.push_play_step(random_red_, 3000)
|
||||
.push_play_step(breathing_blue_, 3000)
|
||||
.push_play_step(dynamic_green_, 3000)
|
||||
|
||||
@ -33,12 +33,12 @@ var forest_anim_ = animation.rich_palette_animation(engine)
|
||||
forest_anim_.palette = animation.PALETTE_FOREST
|
||||
forest_anim_.cycle_period = 8000
|
||||
# Sequence to show both palettes
|
||||
var palette_demo_ = animation.SequenceManager(engine)
|
||||
var palette_demo_ = animation.sequence_manager(engine)
|
||||
.push_play_step(fire_anim_, 10000)
|
||||
.push_wait_step(1000)
|
||||
.push_play_step(ocean_anim_, 10000)
|
||||
.push_wait_step(1000)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, 2)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, 2)
|
||||
.push_play_step(fire_anim_, 3000)
|
||||
.push_play_step(ocean_anim_, 3000)
|
||||
.push_play_step(forest_anim_, 3000)
|
||||
|
||||
@ -72,7 +72,7 @@ sunset_glow_.cycle_period = 6000
|
||||
sunset_glow_.transition_type = animation.SINE
|
||||
sunset_glow_.brightness = 220
|
||||
# Sequence to showcase all palettes
|
||||
var palette_showcase_ = animation.SequenceManager(engine)
|
||||
var palette_showcase_ = animation.sequence_manager(engine)
|
||||
# Fire effect
|
||||
.push_play_step(fire_effect_, 8000)
|
||||
.push_wait_step(1000)
|
||||
@ -86,7 +86,7 @@ var palette_showcase_ = animation.SequenceManager(engine)
|
||||
.push_play_step(sunset_glow_, 8000)
|
||||
.push_wait_step(1000)
|
||||
# Quick cycle through all
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, 3)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, 3)
|
||||
.push_play_step(fire_effect_, 2000)
|
||||
.push_play_step(ocean_waves_, 2000)
|
||||
.push_play_step(aurora_lights_, 2000)
|
||||
|
||||
@ -41,7 +41,7 @@ left_pulse_.priority = 10
|
||||
center_pulse_.priority = 15 # Center has highest priority
|
||||
right_pulse_.priority = 5
|
||||
# Create a sequence that shows all three
|
||||
var demo_ = animation.SequenceManager(engine)
|
||||
var demo_ = animation.sequence_manager(engine)
|
||||
.push_play_step(left_pulse_, 3000)
|
||||
.push_wait_step(500)
|
||||
.push_play_step(center_pulse_, 3000)
|
||||
@ -49,7 +49,7 @@ var demo_ = animation.SequenceManager(engine)
|
||||
.push_play_step(right_pulse_, 3000)
|
||||
.push_wait_step(500)
|
||||
# Play all together for final effect
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, -1)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, -1)
|
||||
.push_play_step(left_pulse_, 2000)
|
||||
.push_play_step(center_pulse_, 2000)
|
||||
.push_play_step(right_pulse_, 2000)
|
||||
|
||||
@ -46,7 +46,7 @@ pulse_demo_.color = 0xFF0000FF
|
||||
pulse_demo_.period = 2000
|
||||
pulse_demo_.priority = 5
|
||||
# Sequence 1: Cylon Eye with Position Changes
|
||||
var cylon_eye_ = animation.SequenceManager(engine)
|
||||
var cylon_eye_ = animation.sequence_manager(engine)
|
||||
.push_play_step(red_eye_, 3000)
|
||||
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end) # Change to triangle oscillator
|
||||
.push_play_step(red_eye_, 3000)
|
||||
@ -54,14 +54,14 @@ var cylon_eye_ = animation.SequenceManager(engine)
|
||||
.push_closure_step(def (engine) eye_color_.next = 1 end) # Advance to next color
|
||||
.push_play_step(red_eye_, 2000)
|
||||
# Sequence 2: Brightness Control Demo
|
||||
var brightness_demo_ = animation.SequenceManager(engine)
|
||||
var brightness_demo_ = animation.sequence_manager(engine)
|
||||
.push_play_step(pulse_demo_, 2000)
|
||||
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # Dim the animation
|
||||
.push_play_step(pulse_demo_, 2000)
|
||||
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Brighten again
|
||||
.push_play_step(pulse_demo_, 2000)
|
||||
# Sequence 3: Multiple Property Changes
|
||||
var multi_change_ = animation.SequenceManager(engine)
|
||||
var multi_change_ = animation.sequence_manager(engine)
|
||||
.push_play_step(pulse_demo_, 1000)
|
||||
.push_closure_step(def (engine) pulse_demo_.color = 0xFFFF0000 end) # Change color
|
||||
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # And brightness
|
||||
@ -71,8 +71,8 @@ var multi_change_ = animation.SequenceManager(engine)
|
||||
.push_play_step(pulse_demo_, 1000)
|
||||
.push_closure_step(def (engine) pulse_demo_.color = 0xFF0000FF end) # Back to blue
|
||||
# Sequence 4: Assignments in Repeat Blocks
|
||||
var repeat_demo_ = animation.SequenceManager(engine)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, 3)
|
||||
var repeat_demo_ = animation.sequence_manager(engine)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, 3)
|
||||
.push_play_step(red_eye_, 1000)
|
||||
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end) # Change oscillator
|
||||
.push_play_step(red_eye_, 1000)
|
||||
@ -80,7 +80,7 @@ var repeat_demo_ = animation.SequenceManager(engine)
|
||||
.push_closure_step(def (engine) eye_color_.next = 1 end) # Next color
|
||||
)
|
||||
# Main demo sequence combining all examples
|
||||
var main_demo_ = animation.SequenceManager(engine)
|
||||
var main_demo_ = animation.sequence_manager(engine)
|
||||
# Run cylon eye demo
|
||||
.push_play_step(red_eye_, 1000)
|
||||
.push_wait_step(500)
|
||||
|
||||
@ -19,7 +19,7 @@ var rainbow_cycle_ = animation.rich_palette_animation(engine)
|
||||
rainbow_cycle_.palette = rainbow_
|
||||
rainbow_cycle_.cycle_period = 3000
|
||||
# Simple sequence
|
||||
var demo_ = animation.SequenceManager(engine)
|
||||
var demo_ = animation.sequence_manager(engine)
|
||||
.push_play_step(rainbow_cycle_, 15000)
|
||||
engine.add(demo_)
|
||||
engine.run()
|
||||
|
||||
@ -17,7 +17,7 @@ olivary_.palette = palette_olivary_
|
||||
olivary_.cycle_period = 0
|
||||
var swipe_animation_ = animation.solid(engine)
|
||||
swipe_animation_.color = olivary_
|
||||
var slide_colors_ = animation.SequenceManager(engine)
|
||||
var slide_colors_ = animation.sequence_manager(engine)
|
||||
.push_play_step(swipe_animation_, 1000)
|
||||
.push_closure_step(def (engine) olivary_.next = 1 end)
|
||||
engine.add(slide_colors_)
|
||||
|
||||
@ -42,15 +42,15 @@ def shutter_bidir_template(engine, colors_, duration_)
|
||||
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
|
||||
shutter_rl_animation_.slew_size = 0 + 0
|
||||
shutter_rl_animation_.priority = 5
|
||||
var shutter_seq_ = animation.SequenceManager(engine, -1)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) log(f"begin 1", 3) end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_lr_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) log(f"begin 2", 3) end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_rl_animation_, animation.resolve(duration_))
|
||||
|
||||
@ -34,7 +34,7 @@ def shutter_central_template(engine, colors_, duration_)
|
||||
shutter_central_animation_.beacon_size = shutter_size_
|
||||
shutter_central_animation_.slew_size = 0
|
||||
shutter_central_animation_.priority = 5
|
||||
var shutter_seq_ = animation.SequenceManager(engine, -1)
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_central_animation_, animation.resolve(duration_))
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
# Generated Berry code from Animation DSL
|
||||
# Source: test_template_animation.anim
|
||||
#
|
||||
# This file was automatically generated by compile_all_examples.sh
|
||||
# Do not edit manually - changes will be overwritten
|
||||
|
||||
import animation
|
||||
|
||||
# Test Template Animation
|
||||
# Simple test to verify template animation syntax
|
||||
# Template animation class: shutter_central
|
||||
class shutter_central_animation : animation.engine_proxy
|
||||
static var PARAMS = animation.enc_params({
|
||||
"colors": {"type": "palette"},
|
||||
"period": {"type": "time"}
|
||||
})
|
||||
|
||||
# Template setup method - overrides EngineProxy placeholder
|
||||
def setup_template()
|
||||
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
|
||||
|
||||
var strip_len_ = animation.strip_length(engine)
|
||||
var strip_len2_ = animation.create_closure_value(engine, def (engine) return (animation.resolve(strip_len_) + 1) / 2 end)
|
||||
var shutter_size_ = (def (engine)
|
||||
var provider = animation.sawtooth(engine)
|
||||
provider.min_value = 0
|
||||
provider.max_value = strip_len_
|
||||
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
|
||||
return provider
|
||||
end)(engine)
|
||||
var col1_ = animation.color_cycle(engine)
|
||||
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
|
||||
col1_.cycle_period = 0
|
||||
var col2_ = animation.color_cycle(engine)
|
||||
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
|
||||
col2_.cycle_period = 0
|
||||
col2_.next = 1
|
||||
# shutter moving in to out
|
||||
var shutter_inout_animation_ = animation.beacon_animation(engine)
|
||||
shutter_inout_animation_.color = col2_
|
||||
shutter_inout_animation_.back_color = col1_
|
||||
shutter_inout_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(shutter_size_) + 1) / 2 end)
|
||||
shutter_inout_animation_.beacon_size = shutter_size_
|
||||
shutter_inout_animation_.slew_size = 0
|
||||
shutter_inout_animation_.priority = 5
|
||||
# shutter moving out to in
|
||||
var shutter_outin_animation_ = animation.beacon_animation(engine)
|
||||
shutter_outin_animation_.color = col1_
|
||||
shutter_outin_animation_.back_color = col2_
|
||||
shutter_outin_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(strip_len_) - animation.resolve(shutter_size_) + 1) / 2 end)
|
||||
shutter_outin_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
|
||||
shutter_outin_animation_.slew_size = 0
|
||||
shutter_outin_animation_.priority = 5
|
||||
var shutter_seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_inout_animation_, def (engine) return self.period end)
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
|
||||
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
|
||||
.push_play_step(shutter_outin_animation_, def (engine) return self.period end)
|
||||
.push_closure_step(def (engine) col1_.next = 1 end)
|
||||
.push_closure_step(def (engine) col2_.next = 1 end)
|
||||
)
|
||||
self.add(shutter_seq_)
|
||||
end
|
||||
end
|
||||
|
||||
# Auto-generated strip initialization (using Tasmota configuration)
|
||||
var engine = animation.init_strip()
|
||||
|
||||
var rainbow_colors_ = bytes("00FF0000" "80008000" "FF0000FF")
|
||||
var my_shutter_ = shutter_central_animation(engine)
|
||||
my_shutter_.colors = rainbow_colors_
|
||||
my_shutter_.period = 2
|
||||
engine.add(my_shutter_)
|
||||
engine.run()
|
||||
|
||||
|
||||
#- Original DSL source:
|
||||
# Test Template Animation
|
||||
# Simple test to verify template animation syntax
|
||||
|
||||
template animation shutter_central {
|
||||
param colors type palette
|
||||
param period type time
|
||||
|
||||
set strip_len = strip_length()
|
||||
set strip_len2 = (strip_len + 1) / 2
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||
|
||||
color col1 = color_cycle(palette=colors, cycle_period=0)
|
||||
color col2 = color_cycle(palette=colors, cycle_period=0)
|
||||
col2.next = 1
|
||||
|
||||
# shutter moving in to out
|
||||
animation shutter_inout_animation = beacon_animation(
|
||||
color = col2
|
||||
back_color = col1
|
||||
pos = strip_len2 - (shutter_size + 1) / 2
|
||||
beacon_size = shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
# shutter moving out to in
|
||||
animation shutter_outin_animation = beacon_animation(
|
||||
color = col1
|
||||
back_color = col2
|
||||
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
|
||||
beacon_size = strip_len - shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence shutter_seq repeat forever {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_inout_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_outin_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
palette rainbow_colors = [
|
||||
(0, red)
|
||||
(128, green)
|
||||
(255, blue)
|
||||
]
|
||||
|
||||
animation my_shutter = shutter_central(colors=rainbow_colors, period=2)
|
||||
run my_shutter
|
||||
|
||||
-#
|
||||
@ -51,13 +51,15 @@ template shutter_bidir {
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
palette rainbow_with_white = [ red
|
||||
orange
|
||||
yellow
|
||||
green, # comma left on-purpose to test transpiler
|
||||
blue # need for a lighter blue
|
||||
indigo
|
||||
white
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
]
|
||||
|
||||
shutter_bidir(rainbow_with_white, 1.5s)
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
# Test Template Animation
|
||||
# Simple test to verify template animation syntax
|
||||
|
||||
template animation shutter_central {
|
||||
param colors type palette
|
||||
param period type time
|
||||
|
||||
set strip_len = strip_length()
|
||||
set strip_len2 = (strip_len + 1) / 2
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||
|
||||
color col1 = color_cycle(palette=colors, cycle_period=0)
|
||||
color col2 = color_cycle(palette=colors, cycle_period=0)
|
||||
col2.next = 1
|
||||
|
||||
# shutter moving in to out
|
||||
animation shutter_inout_animation = beacon_animation(
|
||||
color = col2
|
||||
back_color = col1
|
||||
pos = strip_len2 - (shutter_size + 1) / 2
|
||||
beacon_size = shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
# shutter moving out to in
|
||||
animation shutter_outin_animation = beacon_animation(
|
||||
color = col1
|
||||
back_color = col2
|
||||
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
|
||||
beacon_size = strip_len - shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence shutter_seq repeat forever {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_inout_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_outin_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
palette rainbow_colors = [
|
||||
(0, red)
|
||||
(128, green)
|
||||
(255, blue)
|
||||
]
|
||||
|
||||
animation my_shutter = shutter_central(colors=rainbow_colors, period=2)
|
||||
run my_shutter
|
||||
@ -0,0 +1,4 @@
|
||||
# Plain background
|
||||
|
||||
animation back = solid(color=red)
|
||||
run back
|
||||
@ -0,0 +1,20 @@
|
||||
# Rotation of colors in the background based on palette
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 5 seconds
|
||||
color eye_color = color_cycle(palette=rainbow_with_white, cycle_period=5s)
|
||||
|
||||
animation back = solid(color=eye_color)
|
||||
|
||||
run back
|
||||
@ -0,0 +1,22 @@
|
||||
# Transition of colors in the background based on palette
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
|
||||
animation back = solid(color=rainbow_rich_color)
|
||||
|
||||
run back
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
# Pattern of colors in the background based on palette
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color)
|
||||
|
||||
run back_pattern
|
||||
@ -0,0 +1,25 @@
|
||||
# Pattern of colors in the background based on palette, spatial period = 1/2 strip
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
|
||||
# since strip_length is dynamic, we need to map it to a variable
|
||||
set strip_len = strip_length()
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color, spatial_period = strip_len / 2)
|
||||
|
||||
run back_pattern
|
||||
@ -0,0 +1,28 @@
|
||||
# Pattern of colors in the background based on palette, spatial period oscillating
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
|
||||
# since strip_length is dynamic, we need to map it to a variable
|
||||
set strip_len = strip_length()
|
||||
|
||||
# define the oscillator for spatial period between 1/2 strip_len and 3/2
|
||||
set period = sine_osc(min_value = strip_len / 2, max_value = (3 * strip_len) / 2, duration = 5s)
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color, spatial_period = period)
|
||||
|
||||
run back_pattern
|
||||
@ -0,0 +1,22 @@
|
||||
# Pattern of colors in the background based on palette, rotating over 5 s
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color, shift_period = 5s)
|
||||
|
||||
run back_pattern
|
||||
@ -0,0 +1,27 @@
|
||||
# Pattern of colors in the background based on palette, rotating over 5 s
|
||||
|
||||
berry """
|
||||
def rand_meter(time_ms, self)
|
||||
import math
|
||||
var r = math.rand() % 101
|
||||
return r
|
||||
end
|
||||
"""
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_meter_animation(value_func = rand_meter)
|
||||
|
||||
run back_pattern
|
||||
@ -0,0 +1,23 @@
|
||||
# Template animation for Cylon like eye
|
||||
|
||||
template animation cylon_eye {
|
||||
param eye_color type color default red
|
||||
param back_color type color default transparent
|
||||
param period type time default 5s
|
||||
|
||||
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 = period)
|
||||
beacon_size = 3 # small 3 pixels eye
|
||||
slew_size = 2 # with 2 pixel shading around
|
||||
priority = 5
|
||||
)
|
||||
|
||||
run eye_animation
|
||||
}
|
||||
|
||||
animation eye = cylon_eye()
|
||||
run eye
|
||||
@ -0,0 +1,65 @@
|
||||
# Template animation for Cylon like eye
|
||||
|
||||
template animation shutter_bidir {
|
||||
param colors type palette
|
||||
param period default 5s
|
||||
|
||||
set strip_len = strip_length()
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||
|
||||
color col1 = color_cycle(palette=colors, cycle_period=0)
|
||||
color col2 = color_cycle(palette=colors, cycle_period=0)
|
||||
col2.next = 1
|
||||
|
||||
# shutter moving from left to right
|
||||
animation shutter_lr_animation = beacon_animation(
|
||||
color = col2
|
||||
back_color = col1
|
||||
pos = 0
|
||||
beacon_size = shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
# shutter moving from right to left
|
||||
animation shutter_rl_animation = beacon_animation(
|
||||
color = col1
|
||||
back_color = col2
|
||||
pos = 0
|
||||
beacon_size = strip_len - shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence shutter_seq repeat forever {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_lr_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_rl_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
]
|
||||
|
||||
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
|
||||
run main
|
||||
@ -0,0 +1,71 @@
|
||||
# Template animation with flags
|
||||
|
||||
template animation shutter_bidir {
|
||||
param colors type palette
|
||||
param period default 5s
|
||||
param ascending type bool default true
|
||||
param descending type bool default true
|
||||
|
||||
set strip_len = strip_length()
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||
|
||||
color col1 = color_cycle(palette=colors, cycle_period=0)
|
||||
color col2 = color_cycle(palette=colors, cycle_period=0)
|
||||
col2.next = 1
|
||||
|
||||
# shutter moving from left to right
|
||||
animation shutter_lr_animation = beacon_animation(
|
||||
color = col2
|
||||
back_color = col1
|
||||
pos = 0
|
||||
beacon_size = shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
# shutter moving from right to left
|
||||
animation shutter_rl_animation = beacon_animation(
|
||||
color = col1
|
||||
back_color = col2
|
||||
pos = 0
|
||||
beacon_size = strip_len - shutter_size
|
||||
slew_size = 0
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence shutter_seq repeat forever {
|
||||
repeat ascending times {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_lr_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
repeat descending times {
|
||||
repeat col1.palette_size times {
|
||||
restart shutter_size
|
||||
play shutter_rl_animation for period
|
||||
col1.next = 1
|
||||
col2.next = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run shutter_seq
|
||||
}
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
]
|
||||
|
||||
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
|
||||
run main
|
||||
@ -15,33 +15,35 @@ This document provides a comprehensive reference for all classes in the Berry An
|
||||
|
||||
```
|
||||
ParameterizedObject
|
||||
├── Animation
|
||||
│ ├── BreatheAnimation
|
||||
│ ├── CometAnimation
|
||||
│ ├── FireAnimation
|
||||
│ ├── GradientAnimation
|
||||
│ ├── NoiseAnimation
|
||||
│ ├── BeaconAnimation
|
||||
│ ├── CrenelPositionAnimation
|
||||
│ ├── RichPaletteAnimation
|
||||
│ ├── TwinkleAnimation
|
||||
│ ├── WaveAnimation
|
||||
│ ├── PalettePatternAnimation
|
||||
│ │ ├── PaletteWaveAnimation
|
||||
│ │ ├── PaletteGradientAnimation
|
||||
│ │ └── PaletteMeterAnimation
|
||||
│ └── (other animation classes)
|
||||
└── ValueProvider
|
||||
├── StaticValueProvider
|
||||
├── StripLengthProvider
|
||||
├── OscillatorValueProvider
|
||||
├── ClosureValueProvider (internal use only)
|
||||
└── ColorProvider
|
||||
├── StaticColorProvider
|
||||
├── ColorCycleColorProvider
|
||||
├── RichPaletteColorProvider
|
||||
├── BreatheColorProvider
|
||||
└── CompositeColorProvider
|
||||
├── Playable (base interface for animations and sequences)
|
||||
│ ├── Animation (unified base class for all visual elements)
|
||||
│ │ ├── EngineProxy (combines rendering and orchestration)
|
||||
│ │ │ └── (user-defined template animations)
|
||||
│ │ ├── SolidAnimation (solid color fill)
|
||||
│ │ ├── BeaconAnimation (pulse at specific position)
|
||||
│ │ ├── CrenelPositionAnimation (crenel/square wave pattern)
|
||||
│ │ ├── BreatheAnimation (breathing effect)
|
||||
│ │ ├── PalettePatternAnimation (base for palette-based animations)
|
||||
│ │ ├── CometAnimation (moving comet with tail)
|
||||
│ │ ├── FireAnimation (realistic fire effect)
|
||||
│ │ ├── TwinkleAnimation (twinkling stars effect)
|
||||
│ │ ├── GradientAnimation (color gradients)
|
||||
│ │ ├── NoiseAnimation (Perlin noise patterns)
|
||||
│ │ ├── WaveAnimation (wave motion effects)
|
||||
│ │ └── RichPaletteAnimation (smooth palette transitions)
|
||||
│ └── SequenceManager (orchestrates animation sequences)
|
||||
└── ValueProvider (dynamic value generation)
|
||||
├── StaticValueProvider (wraps static values)
|
||||
├── StripLengthProvider (provides LED strip length)
|
||||
├── IterationNumberProvider (provides sequence iteration number)
|
||||
├── OscillatorValueProvider (oscillating values with waveforms)
|
||||
├── ClosureValueProvider (computed values, internal use only)
|
||||
└── ColorProvider (dynamic color generation)
|
||||
├── StaticColorProvider (solid color)
|
||||
├── ColorCycleColorProvider (cycles through palette)
|
||||
├── RichPaletteColorProvider (smooth palette transitions)
|
||||
├── BreatheColorProvider (breathing color effect)
|
||||
└── CompositeColorProvider (blends multiple colors)
|
||||
```
|
||||
|
||||
## Base Classes
|
||||
@ -76,6 +78,116 @@ Unified base class for all visual elements. Inherits from `ParameterizedObject`.
|
||||
|
||||
**Factory**: `animation.animation(engine)`
|
||||
|
||||
### EngineProxy
|
||||
|
||||
A specialized animation class that combines rendering and orchestration capabilities. Extends `Animation` and can contain child animations and sequences. Inherits from `Animation`.
|
||||
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| *(inherits all Animation parameters)* | | | | |
|
||||
|
||||
**Key Features**:
|
||||
- Can render visual content like a regular animation
|
||||
- Can orchestrate sub-animations and sequences using `add()`
|
||||
- Enables complex composite effects
|
||||
- Used as base class for template animations
|
||||
|
||||
**Child Management**:
|
||||
- `add(playable)` - Adds a child animation or sequence
|
||||
- `remove_child(playable)` - Removes a child
|
||||
- Children are automatically started/stopped with parent
|
||||
- Children are rendered in priority order (higher priority on top)
|
||||
|
||||
**Use Cases**:
|
||||
- Composite effects combining multiple animations
|
||||
- Template animations with parameters
|
||||
- Complex patterns with timing control
|
||||
- Reusable animation components
|
||||
|
||||
**Factory**: `animation.engine_proxy(engine)`
|
||||
|
||||
### Template Animations
|
||||
|
||||
Template animations are user-defined classes that extend `EngineProxy`, created using the DSL's `template animation` syntax. They provide reusable, parameterized animation patterns.
|
||||
|
||||
**DSL Definition**:
|
||||
```berry
|
||||
template animation shutter_effect {
|
||||
param colors type palette nillable true
|
||||
param duration type time min 0 max 3600 default 5 nillable false
|
||||
|
||||
# Animation definition with sequences, colors, etc.
|
||||
# Parameters accessed as self.colors, self.duration
|
||||
}
|
||||
```
|
||||
|
||||
**Generated Class Structure**:
|
||||
```berry
|
||||
class shutter_effect_animation : animation.engine_proxy
|
||||
static var PARAMS = animation.enc_params({
|
||||
"colors": {"type": "palette", "nillable": true},
|
||||
"duration": {"type": "time", "min": 0, "max": 3600, "default": 5, "nillable": false}
|
||||
})
|
||||
|
||||
def init(engine)
|
||||
super(self).init(engine)
|
||||
# Generated code with self.colors and self.duration references
|
||||
# Uses self.add() for sub-animations and sequences
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Parameter Constraints**:
|
||||
Template animation parameters support all standard constraints:
|
||||
- `type` - Parameter type (palette, time, int, color, etc.)
|
||||
- `min` - Minimum value (for numeric types)
|
||||
- `max` - Maximum value (for numeric types)
|
||||
- `default` - Default value
|
||||
- `nillable` - Whether parameter can be nil (true/false)
|
||||
|
||||
**Implicit Parameters**:
|
||||
Template animations automatically inherit parameters from the `EngineProxy` class hierarchy without explicit declaration:
|
||||
- `name` (string, default: "animation") - Animation name
|
||||
- `priority` (int, default: 10) - Rendering priority
|
||||
- `duration` (int, default: 0) - Animation duration in milliseconds
|
||||
- `loop` (bool, default: false) - Whether animation loops
|
||||
- `opacity` (int, default: 255) - Animation opacity (0-255)
|
||||
- `color` (int, default: 0) - Base color value
|
||||
- `is_running` (bool, default: false) - Running state
|
||||
|
||||
These parameters can be used directly in template animation bodies without declaration:
|
||||
```berry
|
||||
template animation fade_effect {
|
||||
param colors type palette
|
||||
|
||||
# 'duration' is implicit - no need to declare
|
||||
set oscillator = sawtooth(min_value=0, max_value=255, duration=duration)
|
||||
|
||||
color col = color_cycle(palette=colors, cycle_period=0)
|
||||
animation test = solid(color=col)
|
||||
test.opacity = oscillator # 'opacity' is also implicit
|
||||
|
||||
run test
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```berry
|
||||
# Create instance with parameters
|
||||
palette rainbow = [red, orange, yellow, green, blue]
|
||||
animation my_shutter = shutter_effect(colors=rainbow, duration=2s)
|
||||
run my_shutter
|
||||
```
|
||||
|
||||
**Key Differences from Regular Animations**:
|
||||
- Defined in DSL, not Berry code
|
||||
- Parameters accessed as `self.<param>` instead of direct variables
|
||||
- Uses `self.add()` for composition
|
||||
- Can be instantiated multiple times with different parameters
|
||||
- Automatically registered as animation constructors
|
||||
|
||||
**Factory**: User-defined (e.g., `shutter_effect(engine)`)
|
||||
|
||||
## Value Providers
|
||||
|
||||
Value providers generate dynamic values over time for use as animation parameters.
|
||||
|
||||
@ -1037,10 +1037,121 @@ palette ocean_palette = [
|
||||
rainbow_pulse(fire_palette, ocean_palette, 3s, black)
|
||||
```
|
||||
|
||||
### Template Behavior
|
||||
### Template Animation
|
||||
|
||||
Template animations create reusable animation classes that extend `engine_proxy`, allowing complex animations with parameters to be instantiated multiple times:
|
||||
|
||||
```berry
|
||||
template animation shutter_effect {
|
||||
param colors type palette nillable true
|
||||
param duration type time min 0 max 3600 default 5 nillable false
|
||||
|
||||
set strip_len = strip_length()
|
||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
|
||||
|
||||
color col = color_cycle(palette=colors, cycle_period=0)
|
||||
|
||||
animation shutter = beacon_animation(
|
||||
color = col
|
||||
pos = strip_len / 2
|
||||
beacon_size = shutter_size
|
||||
priority = 5
|
||||
)
|
||||
|
||||
sequence seq repeat forever {
|
||||
restart shutter_size
|
||||
play shutter for duration
|
||||
col.next = 1
|
||||
}
|
||||
|
||||
run seq
|
||||
}
|
||||
|
||||
# Use the template animation
|
||||
palette rainbow = [red, orange, yellow, green, blue, indigo, white]
|
||||
animation my_shutter = shutter_effect(colors=rainbow, duration=2s)
|
||||
run my_shutter
|
||||
```
|
||||
|
||||
**Code Generation:**
|
||||
Templates generate Berry functions that are registered as user functions:
|
||||
Template animations generate Berry classes extending `engine_proxy`:
|
||||
|
||||
```berry
|
||||
class shutter_effect_animation : animation.engine_proxy
|
||||
static var PARAMS = animation.enc_params({
|
||||
"colors": {"type": "palette"},
|
||||
"duration": {"type": "time", "min": 0, "max": 3600, "default": 5}
|
||||
})
|
||||
|
||||
def init(engine)
|
||||
super(self).init(engine)
|
||||
# Generated code with self.colors and self.duration references
|
||||
self.add(seq_)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Parameter Constraints:**
|
||||
Template animation parameters support constraints:
|
||||
- `type` - Parameter type (palette, time, int, color, etc.)
|
||||
- `min` - Minimum value (for numeric types)
|
||||
- `max` - Maximum value (for numeric types)
|
||||
- `default` - Default value
|
||||
- `nillable` - Whether parameter can be nil (true/false)
|
||||
|
||||
**Implicit Parameters:**
|
||||
Template animations automatically inherit parameters from the `engine_proxy` class hierarchy. These parameters are available without explicit declaration and can be used directly in your template animation body:
|
||||
|
||||
```berry
|
||||
# These parameters are implicitly available in all template animations:
|
||||
param name type string default "animation"
|
||||
param priority type int default 10
|
||||
param duration type int default 0
|
||||
param loop type bool default false
|
||||
param opacity type int default 255
|
||||
param color type int default 0
|
||||
param is_running type bool default false
|
||||
```
|
||||
|
||||
**Example using implicit parameters:**
|
||||
```berry
|
||||
template animation fade_effect {
|
||||
param colors type palette
|
||||
|
||||
# 'duration' is an implicit parameter - no need to declare it
|
||||
set oscillator = sawtooth(min_value=0, max_value=255, duration=duration)
|
||||
|
||||
color col = color_cycle(palette=colors, cycle_period=0)
|
||||
animation test = solid(color=col)
|
||||
|
||||
# 'opacity' is also implicit
|
||||
test.opacity = oscillator
|
||||
|
||||
run test
|
||||
}
|
||||
|
||||
# When instantiating, you can set implicit parameters
|
||||
animation my_fade = fade_effect(colors=rainbow)
|
||||
my_fade.duration = 5000 # Set the implicit duration parameter
|
||||
my_fade.opacity = 200 # Set the implicit opacity parameter
|
||||
```
|
||||
|
||||
**Notes on Implicit Parameters:**
|
||||
- Implicit parameters can be overridden by explicit declarations if needed
|
||||
- They follow the same constraint rules as explicit parameters
|
||||
- They are accessed as `self.<param>` within the template body
|
||||
- All implicit parameters come from the `Animation` and `ParameterizedObject` base classes
|
||||
|
||||
**Key Differences from Regular Templates:**
|
||||
- Generates classes instead of functions
|
||||
- Parameters accessed as `self.<param>` instead of `<param>_`
|
||||
- Uses `self.add()` instead of `engine.add()`
|
||||
- Can be instantiated multiple times with different parameters
|
||||
|
||||
### Regular Template Behavior
|
||||
|
||||
**Code Generation:**
|
||||
Regular templates generate Berry functions that are registered as user functions:
|
||||
|
||||
```berry
|
||||
# Template definition generates:
|
||||
@ -1055,9 +1166,6 @@ end
|
||||
animation.register_user_function('pulse_effect', pulse_effect_template)
|
||||
```
|
||||
|
||||
**Template-Only Transpilation:**
|
||||
Files containing only templates generate pure Berry function definitions without `var engine = animation.init_strip()` or `engine.run()` calls, making them suitable as reusable function libraries.
|
||||
|
||||
**Parameter Handling:**
|
||||
- Parameters get `_` suffix in generated code to avoid naming conflicts
|
||||
- Templates receive `engine` as the first parameter automatically
|
||||
@ -1067,7 +1175,6 @@ Files containing only templates generate pure Berry function definitions without
|
||||
- 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.run()` is automatically called when templates are used at the top level
|
||||
|
||||
### Template Parameter Validation
|
||||
|
||||
@ -1088,7 +1195,26 @@ template bad_example {
|
||||
```
|
||||
|
||||
**Type Annotation Validation:**
|
||||
Valid parameter types are: `color`, `palette`, `animation`, `number`, `string`, `boolean`, `time`, `percentage`, `variable`, `value_provider`
|
||||
|
||||
Valid parameter types for `static var PARAMS` and template parameters:
|
||||
|
||||
| Type | Description | Synonym For | Example |
|
||||
|------|-------------|-------------|---------|
|
||||
| `int` | Integer values | - | `{"type": "int", "default": 100}` |
|
||||
| `bool` | Boolean values | - | `{"type": "bool", "default": false}` |
|
||||
| `string` | String values | - | `{"type": "string", "default": "name"}` |
|
||||
| `bytes` | Byte buffers (palettes) | - | `{"type": "bytes", "default": bytes("FF0000")}` |
|
||||
| `function` | Functions/closures | - | `{"type": "function", "default": nil}` |
|
||||
| `animation` | Animation instances | - | Symbol table tracking |
|
||||
| `value_provider` | Value provider instances | - | Symbol table tracking |
|
||||
| `number` | Generic numeric type | - | Numeric constraints only |
|
||||
| `any` | Any type (no validation) | - | `{"type": "any", "default": nil}` |
|
||||
| `color` | Color values | `int` | `{"type": "color", "default": 0xFFFF0000}` |
|
||||
| `palette` | Palette definitions | `bytes` | `{"type": "palette", "default": bytes(...)}` |
|
||||
| `time` | Time values (ms) | `int` | `{"type": "time", "default": 5000}` |
|
||||
| `percentage` | Percentage (0-255) | `int` | `{"type": "percentage", "default": 128}` |
|
||||
|
||||
**Note:** Types `color`, `palette`, `time`, and `percentage` are user-friendly synonyms that map to their base types during validation.
|
||||
|
||||
```berry
|
||||
template type_example {
|
||||
|
||||
@ -308,11 +308,74 @@ pulse_.priority = 10
|
||||
|
||||
### 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.
|
||||
The DSL supports two types of templates: regular templates (functions) and template animations (classes).
|
||||
|
||||
**Template-Only Files**: DSL files containing only template definitions generate pure Berry function code without engine initialization or execution, creating reusable function libraries.
|
||||
#### Template Animation Transpilation
|
||||
|
||||
#### Template Definition Transpilation
|
||||
Template animations create reusable animation classes extending `engine_proxy`:
|
||||
|
||||
```berry
|
||||
# DSL Template Animation
|
||||
template animation shutter_effect {
|
||||
param colors type palette nillable true
|
||||
param duration type time min 0 max 3600 default 5 nillable false
|
||||
|
||||
set strip_len = strip_length()
|
||||
color col = color_cycle(palette=colors, cycle_period=0)
|
||||
|
||||
animation shutter = beacon_animation(
|
||||
color = col
|
||||
beacon_size = strip_len / 2
|
||||
)
|
||||
|
||||
sequence seq repeat forever {
|
||||
play shutter for duration
|
||||
col.next = 1
|
||||
}
|
||||
|
||||
run seq
|
||||
}
|
||||
```
|
||||
|
||||
**Transpiles to:**
|
||||
|
||||
```berry
|
||||
class shutter_effect_animation : animation.engine_proxy
|
||||
static var PARAMS = animation.enc_params({
|
||||
"colors": {"type": "palette", "nillable": true},
|
||||
"duration": {"type": "time", "min": 0, "max": 3600, "default": 5, "nillable": false}
|
||||
})
|
||||
|
||||
def init(engine)
|
||||
super(self).init(engine)
|
||||
|
||||
var strip_len_ = animation.strip_length(engine)
|
||||
var col_ = animation.color_cycle(engine)
|
||||
col_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
|
||||
col_.cycle_period = 0
|
||||
|
||||
var shutter_ = animation.beacon_animation(engine)
|
||||
shutter_.color = col_
|
||||
shutter_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) / 2 end)
|
||||
|
||||
var seq_ = animation.sequence_manager(engine, -1)
|
||||
.push_play_step(shutter_, animation.resolve(self.duration))
|
||||
.push_closure_step(def (engine) col_.next = 1 end)
|
||||
|
||||
self.add(seq_)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Parameters accessed as `self.<param>` and wrapped in closures
|
||||
- Constraints (min, max, default, nillable) encoded in PARAMS
|
||||
- Uses `self.add()` instead of `engine.add()`
|
||||
- Can be instantiated multiple times with different parameters
|
||||
|
||||
#### Regular Template Transpilation
|
||||
|
||||
Regular templates generate Berry functions:
|
||||
|
||||
```berry
|
||||
# DSL Template
|
||||
@ -320,11 +383,7 @@ template pulse_effect {
|
||||
param color type color
|
||||
param speed
|
||||
|
||||
animation pulse = pulsating_animation(
|
||||
color=color
|
||||
period=speed
|
||||
)
|
||||
|
||||
animation pulse = pulsating_animation(color=color, period=speed)
|
||||
run pulse
|
||||
}
|
||||
```
|
||||
@ -332,89 +391,30 @@ template pulse_effect {
|
||||
**Transpiles to:**
|
||||
|
||||
```berry
|
||||
def pulse_effect(engine, color, speed)
|
||||
def pulse_effect_template(engine, color_, speed_)
|
||||
var pulse_ = animation.pulsating_animation(engine)
|
||||
pulse_.color = color
|
||||
pulse_.period = speed
|
||||
pulse_.color = color_
|
||||
pulse_.period = speed_
|
||||
engine.add(pulse_)
|
||||
engine.run()
|
||||
end
|
||||
|
||||
animation.register_user_function("pulse_effect", pulse_effect)
|
||||
animation.register_user_function('pulse_effect', pulse_effect_template)
|
||||
```
|
||||
|
||||
#### Template Transpilation Process
|
||||
#### Template vs Template Animation
|
||||
|
||||
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 Animation** (`template animation`):
|
||||
- Generates classes extending `engine_proxy`
|
||||
- Parameters accessed as `self.<param>`
|
||||
- Supports parameter constraints (min, max, default, nillable)
|
||||
- Uses `self.add()` for composition
|
||||
- Can be instantiated multiple times
|
||||
|
||||
#### 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(background_)
|
||||
engine.add(comet_)
|
||||
engine.run()
|
||||
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
|
||||
- Template-only files generate pure function libraries
|
||||
|
||||
**User Functions** (Berry-native):
|
||||
- Defined in Berry code
|
||||
- Use Berry syntax
|
||||
- Manually registered
|
||||
- Full Berry language features
|
||||
- Called from DSL
|
||||
**Regular Template** (`template`):
|
||||
- Generates functions
|
||||
- Parameters accessed as `<param>_`
|
||||
- Uses `engine.add()` for execution
|
||||
- Called like functions
|
||||
|
||||
### User-Defined Functions
|
||||
|
||||
|
||||
@ -178,63 +178,66 @@ var runtime = animation.load_dsl_file("my_animation.anim")
|
||||
|
||||
## Templates - Reusable Animation Patterns
|
||||
|
||||
Templates let you create reusable animation patterns with parameters:
|
||||
### Template Animations
|
||||
|
||||
Template animations create reusable animation classes with parameters:
|
||||
|
||||
```berry
|
||||
# Define a template animation with constraints
|
||||
template animation shutter_effect {
|
||||
param colors type palette nillable true
|
||||
param duration type time min 0 max 3600 default 5 nillable false
|
||||
|
||||
set strip_len = strip_length()
|
||||
color col = color_cycle(palette=colors, cycle_period=0)
|
||||
|
||||
animation shutter = beacon_animation(
|
||||
color = col
|
||||
beacon_size = strip_len / 2
|
||||
)
|
||||
|
||||
sequence seq repeat forever {
|
||||
play shutter for duration
|
||||
col.next = 1
|
||||
}
|
||||
|
||||
run seq
|
||||
}
|
||||
|
||||
# Create multiple instances with different parameters
|
||||
palette rainbow = [red, orange, yellow, green, blue]
|
||||
animation shutter1 = shutter_effect(colors=rainbow, duration=2s)
|
||||
animation shutter2 = shutter_effect(colors=rainbow, duration=5s)
|
||||
|
||||
run shutter1
|
||||
run shutter2
|
||||
```
|
||||
|
||||
**Template Animation Features:**
|
||||
- **Reusable Classes** - Create multiple instances with different parameters
|
||||
- **Parameter Constraints** - min, max, default, nillable values
|
||||
- **Composition** - Combine multiple animations and sequences
|
||||
- **Type Safe** - Parameter type checking
|
||||
- **Implicit Parameters** - Automatically inherit parameters from base classes (name, priority, duration, loop, opacity, color, is_running)
|
||||
|
||||
### Regular Templates
|
||||
|
||||
Regular templates generate functions for simpler use cases:
|
||||
|
||||
```berry
|
||||
# Define a template for pulsing effects
|
||||
template pulse_effect {
|
||||
param color type color
|
||||
param speed
|
||||
|
||||
animation pulse = pulsating_animation(
|
||||
color=color
|
||||
period=speed
|
||||
)
|
||||
|
||||
animation pulse = pulsating_animation(color=color, period=speed)
|
||||
run pulse
|
||||
}
|
||||
|
||||
# Use the template with different parameters
|
||||
# Use the template
|
||||
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:
|
||||
|
||||
@ -107,13 +107,13 @@ process_sequence()
|
||||
│ ├── "sequence name N times { ... }"
|
||||
│ └── "sequence name { repeat ... }"
|
||||
├── expect_left_brace() → '{'
|
||||
├── add("var name_ = animation.SequenceManager(engine, repeat_count)")
|
||||
├── add("var name_ = animation.sequence_manager(engine, repeat_count)")
|
||||
├── while !check_right_brace()
|
||||
│ └── process_sequence_statement() (fluent interface)
|
||||
└── expect_right_brace() → '}'
|
||||
```
|
||||
|
||||
#### Template Processing (New)
|
||||
#### Template Processing
|
||||
```
|
||||
process_template()
|
||||
├── expect_identifier() → template name
|
||||
@ -129,6 +129,55 @@ process_template()
|
||||
│ ├── Generate Berry function with engine parameter
|
||||
│ └── Register as user function
|
||||
└── Track in symbol_table as "template"
|
||||
|
||||
process_template_animation()
|
||||
├── expect_identifier() → template animation name
|
||||
├── validate_user_name() → check against reserved names
|
||||
├── expect_left_brace() → '{'
|
||||
├── Sequential step 1: collect parameters with constraints (type, min, max, default)
|
||||
├── Sequential step 2: collect body tokens
|
||||
├── expect_right_brace() → '}'
|
||||
├── generate_template_animation_class()
|
||||
│ ├── Generate class extending engine_proxy
|
||||
│ ├── Generate PARAMS with encode_constraints
|
||||
│ ├── Create new transpiler instance for body
|
||||
│ ├── Set template_animation_params for special handling
|
||||
│ │ ├── Add user-defined parameters
|
||||
│ │ └── Add inherited parameters from engine_proxy hierarchy (dynamic discovery)
|
||||
│ ├── Transpile body with self.param references
|
||||
│ └── Use self.add() instead of engine.add()
|
||||
└── Track in symbol_table as "template"
|
||||
|
||||
### Implicit Parameters in Template Animations
|
||||
|
||||
Template animations automatically inherit parameters from the `engine_proxy` class hierarchy. The transpiler dynamically discovers these parameters at compile time:
|
||||
|
||||
**Dynamic Parameter Discovery:**
|
||||
```
|
||||
_add_inherited_params_to_template(template_params_map)
|
||||
├── Create temporary engine_proxy instance
|
||||
├── Walk up class hierarchy using introspection
|
||||
├── For each class with PARAMS:
|
||||
│ └── Add all parameter names to template_params_map
|
||||
└── Fallback to static list if instance creation fails
|
||||
```
|
||||
|
||||
**Inherited Parameters (from Animation and ParameterizedObject):**
|
||||
- `name` (string, default: "animation")
|
||||
- `priority` (int, default: 10)
|
||||
- `duration` (int, default: 0)
|
||||
- `loop` (bool, default: false)
|
||||
- `opacity` (int, default: 255)
|
||||
- `color` (int, default: 0)
|
||||
- `is_running` (bool, default: false)
|
||||
|
||||
**Parameter Resolution Order:**
|
||||
1. Check if identifier is in `template_animation_params` (includes both user-defined and inherited)
|
||||
2. If found, resolve as `self.<param>` (template animation parameter)
|
||||
3. Otherwise, check symbol table for user-defined variables
|
||||
4. If not found, raise "Unknown identifier" error
|
||||
|
||||
This allows template animations to use inherited parameters like `duration` and `opacity` without explicit declaration, while still maintaining type safety and validation.
|
||||
```
|
||||
|
||||
## Expression Processing Chain
|
||||
@ -556,13 +605,13 @@ Sequences use fluent interface pattern for better readability:
|
||||
```berry
|
||||
# DSL: sequence demo { play anim for 2s; wait 1s }
|
||||
# Generated:
|
||||
var demo_ = animation.SequenceManager(engine)
|
||||
var demo_ = animation.sequence_manager(engine)
|
||||
.push_play_step(anim_, 2000)
|
||||
.push_wait_step(1000)
|
||||
|
||||
# Nested repeats use sub-sequences:
|
||||
var demo_ = animation.SequenceManager(engine)
|
||||
.push_repeat_subsequence(animation.SequenceManager(engine, 3)
|
||||
var demo_ = animation.sequence_manager(engine)
|
||||
.push_repeat_subsequence(animation.sequence_manager(engine, 3)
|
||||
.push_play_step(anim_, 1000)
|
||||
)
|
||||
```
|
||||
|
||||
@ -595,6 +595,185 @@ animation.register_user_function("pulse_effect", create_pulse_effect)
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### CPU Metrics and Profiling
|
||||
|
||||
**Feature:** Built-in CPU metrics tracking to monitor animation performance
|
||||
|
||||
The AnimationEngine automatically tracks CPU usage and provides detailed statistics every 5 seconds. This helps identify performance bottlenecks and optimize animations for ESP32 embedded systems.
|
||||
|
||||
**Automatic Metrics:**
|
||||
|
||||
When the engine is running, it automatically logs performance statistics:
|
||||
|
||||
```
|
||||
AnimEngine: ticks=1000/1000 missed=0 total=0.50ms(0-2) anim=0.30ms(0-1) hw=0.20ms(0-1) cpu=10.0%
|
||||
```
|
||||
|
||||
**Metrics Explained:**
|
||||
- **ticks**: Actual ticks executed vs expected (at 5ms intervals)
|
||||
- **missed**: Hint of missed ticks (negative means extra ticks, positive means missed)
|
||||
- **total**: Mean total tick time with (min-max) range in milliseconds
|
||||
- **anim**: Mean animation calculation time with (min-max) range - everything before hardware output
|
||||
- **hw**: Mean hardware output time with (min-max) range - just the LED strip update
|
||||
- **cpu**: Overall CPU usage percentage over the 5-second period
|
||||
|
||||
**Custom Profiling API:**
|
||||
|
||||
For measuring specific code sections, use the profiling API:
|
||||
|
||||
```berry
|
||||
import animation
|
||||
|
||||
var strip = Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Start measuring a code section
|
||||
engine.profile_start("my_calculation")
|
||||
|
||||
# Your code to measure
|
||||
var result = 0
|
||||
var i = 0
|
||||
while i < 1000
|
||||
result += i
|
||||
i += 1
|
||||
end
|
||||
|
||||
# End measuring
|
||||
engine.profile_end("my_calculation")
|
||||
|
||||
# Run the engine - stats will be printed every 5 seconds
|
||||
engine.run()
|
||||
```
|
||||
|
||||
**Profiling Output:**
|
||||
|
||||
Custom profiling points appear in the stats output:
|
||||
|
||||
```
|
||||
AnimEngine: ticks=1000/1000 missed=0 total=0.50ms(0-2) anim=0.30ms(0-1) hw=0.20ms(0-1) cpu=10.0%
|
||||
Profile[my_calculation]: count=1000 mean=0.15ms min=0ms max=1ms
|
||||
Profile[another_section]: count=500 mean=0.25ms min=0ms max=2ms
|
||||
```
|
||||
|
||||
**Profiling Best Practices:**
|
||||
|
||||
1. **Use Descriptive Names:**
|
||||
```berry
|
||||
engine.profile_start("render_effects")
|
||||
# ... rendering code ...
|
||||
engine.profile_end("render_effects")
|
||||
|
||||
engine.profile_start("color_calculation")
|
||||
# ... color processing ...
|
||||
engine.profile_end("color_calculation")
|
||||
```
|
||||
|
||||
2. **Profile Critical Sections:**
|
||||
```berry
|
||||
# Measure custom effect rendering
|
||||
def my_custom_effect(frame, time_ms)
|
||||
self.engine.profile_start("custom_effect")
|
||||
|
||||
# Your effect logic
|
||||
var i = 0
|
||||
while i < frame.width
|
||||
var color = calculate_color(i, time_ms)
|
||||
frame.set_pixel_color(i, color)
|
||||
i += 1
|
||||
end
|
||||
|
||||
self.engine.profile_end("custom_effect")
|
||||
end
|
||||
```
|
||||
|
||||
3. **Avoid Profiling in Tight Loops:**
|
||||
```berry
|
||||
# ❌ BAD - profiling overhead in loop
|
||||
var i = 0
|
||||
while i < 1000
|
||||
engine.profile_start("loop_iteration")
|
||||
# ... work ...
|
||||
engine.profile_end("loop_iteration")
|
||||
i += 1
|
||||
end
|
||||
|
||||
# ✅ GOOD - profile entire loop
|
||||
engine.profile_start("entire_loop")
|
||||
var i = 0
|
||||
while i < 1000
|
||||
# ... work ...
|
||||
i += 1
|
||||
end
|
||||
engine.profile_end("entire_loop")
|
||||
```
|
||||
|
||||
4. **Multiple Profiling Points:**
|
||||
```berry
|
||||
# You can have multiple active profiling points
|
||||
engine.profile_start("section_a")
|
||||
# ... code A ...
|
||||
engine.profile_end("section_a")
|
||||
|
||||
engine.profile_start("section_b")
|
||||
# ... code B ...
|
||||
engine.profile_end("section_b")
|
||||
```
|
||||
|
||||
**Interpreting Performance Metrics:**
|
||||
|
||||
1. **High Animation Time:**
|
||||
- Too many simultaneous animations
|
||||
- Complex value provider calculations
|
||||
- Inefficient custom effects
|
||||
|
||||
**Solution:** Simplify animations or use sequences
|
||||
|
||||
2. **High Hardware Time:**
|
||||
- Large LED strip (many pixels)
|
||||
- Slow SPI/I2C communication
|
||||
- Hardware limitations
|
||||
|
||||
**Solution:** Reduce update frequency or strip length
|
||||
|
||||
3. **Missed Ticks:**
|
||||
- CPU overload (total time > 5ms per tick)
|
||||
- Other Tasmota tasks interfering
|
||||
|
||||
**Solution:** Optimize animations or reduce complexity
|
||||
|
||||
4. **High CPU Percentage:**
|
||||
- Animations consuming too much CPU
|
||||
- May affect other Tasmota functions
|
||||
|
||||
**Solution:** Increase animation periods or reduce effects
|
||||
|
||||
**Example Performance Optimization:**
|
||||
|
||||
```berry
|
||||
import animation
|
||||
|
||||
var strip = Leds(60)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Before optimization - complex animation
|
||||
var complex_anim = animation.rainbow_animation(engine)
|
||||
complex_anim.period = 100 # Very fast, high CPU
|
||||
|
||||
engine.add(complex_anim)
|
||||
engine.run()
|
||||
|
||||
# Check metrics after 5 seconds:
|
||||
# AnimEngine: ticks=950/1000 missed=50 total=5.2ms(4-8) cpu=104.0%
|
||||
# ^ Too slow! Missing ticks and over 100% CPU
|
||||
|
||||
# After optimization - slower period
|
||||
complex_anim.period = 2000 # 2 seconds instead of 100ms
|
||||
|
||||
# Check metrics after 5 seconds:
|
||||
# AnimEngine: ticks=1000/1000 missed=0 total=0.8ms(0-2) cpu=16.0%
|
||||
# ^ Much better! All ticks processed, reasonable CPU usage
|
||||
```
|
||||
|
||||
### Choppy Animations
|
||||
|
||||
**Problem:** Animations appear jerky or stuttering
|
||||
@ -641,6 +820,19 @@ animation.register_user_function("pulse_effect", create_pulse_effect)
|
||||
anim2.opacity = breathing # Reuse same provider
|
||||
```
|
||||
|
||||
4. **Monitor CPU Metrics:**
|
||||
```berry
|
||||
# Check if CPU is overloaded
|
||||
# Look for missed ticks or high CPU percentage in metrics
|
||||
# AnimEngine: ticks=950/1000 missed=50 ... cpu=95.0%
|
||||
# ^ This indicates performance issues
|
||||
|
||||
# Use profiling to find bottlenecks
|
||||
engine.profile_start("suspect_code")
|
||||
# ... code that might be slow ...
|
||||
engine.profile_end("suspect_code")
|
||||
```
|
||||
|
||||
### Memory Issues
|
||||
|
||||
**Problem:** Out of memory errors or system crashes
|
||||
|
||||
@ -57,6 +57,10 @@ end
|
||||
# Import core framework components
|
||||
# These provide the fundamental architecture for the animation system
|
||||
|
||||
# Parameter constraint encoder for PARAMS definitions
|
||||
import "core/param_encoder" as param_encoder
|
||||
register_to_animation(param_encoder)
|
||||
|
||||
# Mathematical functions for use in closures and throughout the framework
|
||||
import "core/math_functions" as math_functions
|
||||
register_to_animation(math_functions)
|
||||
@ -69,6 +73,10 @@ register_to_animation(parameterized_object)
|
||||
import "core/frame_buffer" as frame_buffer
|
||||
register_to_animation(frame_buffer)
|
||||
|
||||
# Playable base class - common interface for animations and sequences
|
||||
import "core/playable_base" as playable_base
|
||||
register_to_animation(playable_base)
|
||||
|
||||
# Base Animation class - unified foundation for all visual elements
|
||||
import "core/animation_base" as animation_base
|
||||
register_to_animation(animation_base)
|
||||
@ -77,6 +85,10 @@ register_to_animation(animation_base)
|
||||
import "core/sequence_manager" as sequence_manager
|
||||
register_to_animation(sequence_manager)
|
||||
|
||||
# Engine proxy - combines rendering and orchestration
|
||||
import "core/engine_proxy" as engine_proxy
|
||||
register_to_animation(engine_proxy)
|
||||
|
||||
# Unified animation engine - central engine for all animations
|
||||
# Provides priority-based layering, automatic blending, and performance optimization
|
||||
import "core/animation_engine" as animation_engine
|
||||
|
||||
@ -24,7 +24,7 @@ class BeaconAnimation : animation.animation
|
||||
# NO instance variables for parameters - they are handled by the virtual parameter system
|
||||
|
||||
# Parameter definitions following the new specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": 0xFFFFFFFF},
|
||||
"back_color": {"default": 0xFF000000},
|
||||
"pos": {"default": 0},
|
||||
|
||||
@ -16,7 +16,7 @@ class BreatheAnimation : animation.animation
|
||||
var breathe_provider # Internal breathe color provider
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"base_color": {"default": 0xFFFFFFFF}, # The base color to breathe (32-bit ARGB value)
|
||||
"min_brightness": {"min": 0, "max": 255, "default": 0}, # Minimum brightness level (0-255)
|
||||
"max_brightness": {"min": 0, "max": 255, "default": 255}, # Maximum brightness level (0-255)
|
||||
|
||||
@ -14,7 +14,7 @@ class CometAnimation : animation.animation
|
||||
var head_position # Current position of the comet head (in 1/256th pixels for smooth movement)
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# 'color' for the comet head (32-bit ARGB value), inherited from animation class
|
||||
"tail_length": {"min": 1, "max": 50, "default": 5}, # Length of the comet tail in pixels
|
||||
"speed": {"min": 1, "max": 25600, "default": 2560}, # Movement speed in 1/256th pixels per second
|
||||
|
||||
@ -25,7 +25,7 @@ class CrenelPositionAnimation : animation.animation
|
||||
# NO instance variables for parameters - they are handled by the virtual parameter system
|
||||
|
||||
# Parameter definitions with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# 'color' for the comet head (32-bit ARGB value), inherited from animation class
|
||||
"back_color": {"default": 0xFF000000}, # background color, TODO change to transparent
|
||||
"pos": {"default": 0}, # start of the pulse (in pixel)
|
||||
|
||||
@ -14,7 +14,7 @@ class FireAnimation : animation.animation
|
||||
var random_seed # Seed for random number generation
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# 'color' for the comet head (32-bit ARGB value), inherited from animation class
|
||||
"intensity": {"min": 0, "max": 255, "default": 180},
|
||||
"flicker_speed": {"min": 1, "max": 20, "default": 8},
|
||||
|
||||
@ -12,7 +12,7 @@ class GradientAnimation : animation.animation
|
||||
var phase_offset # Current phase offset for movement
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": nil, "nillable": true},
|
||||
"gradient_type": {"min": 0, "max": 1, "default": 0},
|
||||
"direction": {"min": 0, "max": 255, "default": 0},
|
||||
|
||||
@ -13,7 +13,7 @@ class NoiseAnimation : animation.animation
|
||||
var noise_table # Pre-computed noise values for performance
|
||||
|
||||
# Parameter definitions following new specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": nil},
|
||||
"scale": {"min": 1, "max": 255, "default": 50},
|
||||
"speed": {"min": 0, "max": 255, "default": 30},
|
||||
|
||||
@ -13,7 +13,7 @@ class PalettePatternAnimation : animation.animation
|
||||
var value_buffer # Buffer to store values for each pixel (bytes object)
|
||||
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Palette pattern-specific parameters
|
||||
"color_source": {"default": nil, "type": "instance"},
|
||||
"pattern_func": {"default": nil, "type": "function"}
|
||||
@ -160,7 +160,7 @@ end
|
||||
#@ solidify:PaletteWaveAnimation,weak
|
||||
class PaletteWaveAnimation : PalettePatternAnimation
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Wave-specific parameters only
|
||||
"wave_period": {"min": 1, "default": 5000},
|
||||
"wave_length": {"min": 1, "default": 10}
|
||||
@ -213,7 +213,7 @@ end
|
||||
#@ solidify:PaletteGradientAnimation,weak
|
||||
class PaletteGradientAnimation : PalettePatternAnimation
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Gradient-specific parameters only
|
||||
"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)
|
||||
@ -275,7 +275,7 @@ end
|
||||
#@ solidify:PaletteMeterAnimation,weak
|
||||
class PaletteMeterAnimation : PalettePatternAnimation
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Meter-specific parameters only
|
||||
"value_func": {"default": nil, "type": "function"}
|
||||
})
|
||||
|
||||
@ -15,7 +15,7 @@ class RichPaletteAnimation : animation.animation
|
||||
var color_provider # Internal RichPaletteColorProvider instance
|
||||
|
||||
# Parameter definitions - only RichPaletteColorProvider parameters (Animation params inherited)
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
# RichPaletteColorProvider parameters (forwarded to internal provider)
|
||||
"palette": {"type": "instance", "default": nil},
|
||||
"cycle_period": {"min": 0, "default": 5000},
|
||||
|
||||
@ -16,7 +16,7 @@ class TwinkleAnimation : animation.animation
|
||||
var random_seed # Seed for random number generation
|
||||
|
||||
# Parameter definitions with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": 0xFFFFFFFF},
|
||||
"density": {"min": 0, "max": 255, "default": 128},
|
||||
"twinkle_speed": {"min": 1, "max": 5000, "default": 6},
|
||||
|
||||
@ -13,7 +13,7 @@ class WaveAnimation : animation.animation
|
||||
var wave_table # Pre-computed wave table for performance
|
||||
|
||||
# Parameter definitions for WaveAnimation
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": 0xFFFF0000},
|
||||
"back_color": {"default": 0xFF000000},
|
||||
"wave_type": {"min": 0, "max": 3, "default": 0},
|
||||
|
||||
@ -16,7 +16,7 @@ class BounceAnimation : animation.animation
|
||||
var last_update_time # Last update time for physics calculation
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"source_animation": {"type": "instance", "default": nil},
|
||||
"bounce_speed": {"min": 0, "max": 255, "default": 128},
|
||||
"bounce_range": {"min": 0, "max": 1000, "default": 0},
|
||||
|
||||
@ -15,7 +15,7 @@ class JitterAnimation : animation.animation
|
||||
var current_colors # Array of current colors for each pixel
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"source_animation": {"type": "instance", "default": nil},
|
||||
"jitter_intensity": {"min": 0, "max": 255, "default": 100},
|
||||
"jitter_frequency": {"min": 0, "max": 255, "default": 60},
|
||||
|
||||
@ -12,7 +12,7 @@ class PlasmaAnimation : animation.animation
|
||||
var time_phase # Current time-based phase
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": nil},
|
||||
"freq_x": {"min": 1, "max": 255, "default": 32},
|
||||
"freq_y": {"min": 1, "max": 255, "default": 23},
|
||||
|
||||
@ -14,7 +14,7 @@ class ScaleAnimation : animation.animation
|
||||
var start_time # Animation start time
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"source_animation": {"type": "instance", "default": nil},
|
||||
"scale_factor": {"min": 1, "max": 255, "default": 128},
|
||||
"scale_speed": {"min": 0, "max": 255, "default": 0},
|
||||
|
||||
@ -13,7 +13,7 @@ class ShiftAnimation : animation.animation
|
||||
var current_colors # Array of current colors for each pixel
|
||||
|
||||
# Parameter definitions with constraints
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"source_animation": {"type": "instance", "default": nil},
|
||||
"shift_speed": {"min": 0, "max": 255, "default": 128},
|
||||
"direction": {"min": -1, "max": 1, "default": 1},
|
||||
|
||||
@ -15,7 +15,7 @@ class SparkleAnimation : animation.animation
|
||||
var last_update # Last update time for frame timing
|
||||
|
||||
# Parameter definitions following parameterized class specification
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": 0xFFFFFFFF},
|
||||
"back_color": {"default": 0xFF000000},
|
||||
"density": {"min": 0, "max": 255, "default": 30},
|
||||
|
||||
@ -6,21 +6,24 @@
|
||||
#
|
||||
# This is the unified base class for all visual elements in the framework.
|
||||
# A Pattern is simply an Animation with infinite duration (duration = 0).
|
||||
#
|
||||
# Extends Playable to provide the common interface for lifecycle management.
|
||||
|
||||
import "./core/param_encoder" as encode_constraints
|
||||
|
||||
class Animation : animation.parameterized_object
|
||||
class Animation : animation.playable
|
||||
# Non-parameter instance variables only
|
||||
var opacity_frame # Frame buffer for opacity animation rendering
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
# Parameter definitions (extends Playable's PARAMS)
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Inherited from Playable: is_running
|
||||
"name": {"type": "string", "default": "animation"}, # Optional name for the animation
|
||||
"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)
|
||||
"color": {"default": 0x00000000} # Base color in ARGB format (0xAARRGGBB) - default to transparent
|
||||
})
|
||||
|
||||
# Initialize a new animation
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
# Unified Animation Engine
|
||||
#
|
||||
# Uses composition pattern: contains a root EngineProxy that manages all children.
|
||||
# The engine provides infrastructure (strip output, fast_loop) while delegating
|
||||
# child management and rendering to the root animation.
|
||||
|
||||
class AnimationEngine
|
||||
# Core properties
|
||||
var strip # LED strip object
|
||||
var width # Strip width (cached for performance)
|
||||
var animations # List of active animations (sorted by priority)
|
||||
var sequence_managers # List of active sequence managers
|
||||
var root_animation # Root EngineProxy that holds all children
|
||||
var frame_buffer # Main frame buffer
|
||||
var temp_buffer # Temporary buffer for blending
|
||||
|
||||
@ -19,8 +21,23 @@ class AnimationEngine
|
||||
# Performance optimization
|
||||
var render_needed # Whether a render pass is needed
|
||||
|
||||
# Sequence iteration tracking (stack-based for nested sequences)
|
||||
var iteration_stack # Stack of iteration numbers for nested sequences
|
||||
# CPU metrics tracking (streaming stats - no array storage)
|
||||
var tick_count # Number of ticks in current period
|
||||
var tick_time_sum # Sum of all tick times (for mean calculation)
|
||||
var tick_time_min # Minimum tick time in period
|
||||
var tick_time_max # Maximum tick time in period
|
||||
var anim_time_sum # Sum of animation calculation times
|
||||
var anim_time_min # Minimum animation calculation time
|
||||
var anim_time_max # Maximum animation calculation time
|
||||
var hw_time_sum # Sum of hardware output times
|
||||
var hw_time_min # Minimum hardware output time
|
||||
var hw_time_max # Maximum hardware output time
|
||||
var last_stats_time # Last time stats were printed
|
||||
var stats_period # Stats reporting period (5000ms)
|
||||
|
||||
# Custom profiling points
|
||||
var profile_points # Map of profile point name -> {count, sum, min, max}
|
||||
var profile_start_times # Map of profile point name -> start time
|
||||
|
||||
# Initialize the animation engine for a specific LED strip
|
||||
def init(strip)
|
||||
@ -30,8 +47,10 @@ class AnimationEngine
|
||||
|
||||
self.strip = strip
|
||||
self.width = strip.length()
|
||||
self.animations = []
|
||||
self.sequence_managers = []
|
||||
|
||||
# Create root EngineProxy to manage all children
|
||||
self.root_animation = animation.engine_proxy(self)
|
||||
self.root_animation.name = "root"
|
||||
|
||||
# Create frame buffers
|
||||
self.frame_buffer = animation.frame_buffer(self.width)
|
||||
@ -44,8 +63,23 @@ class AnimationEngine
|
||||
self.fast_loop_closure = nil
|
||||
self.render_needed = false
|
||||
|
||||
# Initialize iteration tracking stack
|
||||
self.iteration_stack = []
|
||||
# Initialize CPU metrics
|
||||
self.tick_count = 0
|
||||
self.tick_time_sum = 0
|
||||
self.tick_time_min = 999999
|
||||
self.tick_time_max = 0
|
||||
self.anim_time_sum = 0
|
||||
self.anim_time_min = 999999
|
||||
self.anim_time_max = 0
|
||||
self.hw_time_sum = 0
|
||||
self.hw_time_min = 999999
|
||||
self.hw_time_max = 0
|
||||
self.last_stats_time = 0
|
||||
self.stats_period = 5000
|
||||
|
||||
# Initialize custom profiling
|
||||
self.profile_points = {}
|
||||
self.profile_start_times = {}
|
||||
end
|
||||
|
||||
# Run the animation engine
|
||||
@ -61,17 +95,8 @@ class AnimationEngine
|
||||
self.fast_loop_closure = / -> self.on_tick()
|
||||
end
|
||||
|
||||
var i = 0
|
||||
while (i < size(self.animations))
|
||||
self.animations[i].start(now)
|
||||
i += 1
|
||||
end
|
||||
|
||||
i = 0
|
||||
while (i < size(self.sequence_managers))
|
||||
self.sequence_managers[i].start(now)
|
||||
i += 1
|
||||
end
|
||||
# Start the root animation (which starts all children)
|
||||
self.root_animation.start(now)
|
||||
|
||||
tasmota.add_fast_loop(self.fast_loop_closure)
|
||||
end
|
||||
@ -92,124 +117,49 @@ class AnimationEngine
|
||||
return self
|
||||
end
|
||||
|
||||
# Add an animation with automatic priority sorting
|
||||
# Add a playable object (animation or sequence) to the root animation
|
||||
#
|
||||
# @param anim: animation - The animation instance to add (if not already listed)
|
||||
# @return true if succesful (TODO always true)
|
||||
def _add_animation(anim)
|
||||
if (self.animations.find(anim) == nil) # not already in list
|
||||
# Add and sort by priority (higher priority first)
|
||||
self.animations.push(anim)
|
||||
self._sort_animations()
|
||||
# If the engine is already started, auto-start the animation
|
||||
if self.is_running
|
||||
anim.start(self.time_ms)
|
||||
end
|
||||
# @param obj: Playable - The playable object to add
|
||||
# @return bool - True if added, false if already exists
|
||||
def add(obj)
|
||||
var ret = self.root_animation.add(obj)
|
||||
if ret
|
||||
self.render_needed = true
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
# Remove an animation
|
||||
def remove_animation(animation)
|
||||
var index = -1
|
||||
var i = 0
|
||||
while i < size(self.animations)
|
||||
if self.animations[i] == animation
|
||||
index = i
|
||||
break
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
|
||||
if index >= 0
|
||||
self.animations.remove(index)
|
||||
# Remove a playable object from the root animation
|
||||
#
|
||||
# @param obj: Playable - The playable object to remove
|
||||
# @return bool - True if removed, false if not found
|
||||
def remove(obj)
|
||||
var ret = self.root_animation.remove(obj)
|
||||
if ret
|
||||
self.render_needed = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
return ret
|
||||
end
|
||||
|
||||
# Clear all animations and sequences
|
||||
def clear()
|
||||
self.animations = []
|
||||
var i = 0
|
||||
while i < size(self.sequence_managers)
|
||||
self.sequence_managers[i].stop()
|
||||
i += 1
|
||||
end
|
||||
self.sequence_managers = []
|
||||
# Stop and clear all children in root animation
|
||||
self.root_animation.clear()
|
||||
self.render_needed = true
|
||||
return self
|
||||
end
|
||||
|
||||
# Add a sequence manager
|
||||
def _add_sequence_manager(sequence_manager)
|
||||
self.sequence_managers.push(sequence_manager)
|
||||
return self
|
||||
end
|
||||
|
||||
# Unified method to add either animations or sequence managers
|
||||
# Detects the class type and calls the appropriate method
|
||||
#
|
||||
# @param obj: Animation or SequenceManager - The object to add
|
||||
# @return self for method chaining
|
||||
def add(obj)
|
||||
# Check if it's a SequenceManager
|
||||
if isinstance(obj, animation.SequenceManager)
|
||||
return self._add_sequence_manager(obj)
|
||||
# Check if it's an Animation (or subclass)
|
||||
elif isinstance(obj, animation.animation)
|
||||
return self._add_animation(obj)
|
||||
else
|
||||
# Unknown type - provide helpful error message
|
||||
raise "type_error", "only Animation or SequenceManager"
|
||||
end
|
||||
end
|
||||
|
||||
# Generic remove method that delegates to specific remove methods
|
||||
# @param obj: Animation or SequenceManager - The object to remove
|
||||
# @return self for method chaining
|
||||
def remove(obj)
|
||||
# Check if it's a SequenceManager
|
||||
if isinstance(obj, animation.SequenceManager)
|
||||
return self.remove_sequence_manager(obj)
|
||||
# Check if it's an Animation (or subclass)
|
||||
elif isinstance(obj, animation.animation)
|
||||
return self.remove_animation(obj)
|
||||
else
|
||||
# Unknown type - ignore
|
||||
end
|
||||
end
|
||||
|
||||
# Remove a sequence manager
|
||||
def remove_sequence_manager(sequence_manager)
|
||||
var index = -1
|
||||
var i = 0
|
||||
while i < size(self.sequence_managers)
|
||||
if self.sequence_managers[i] == sequence_manager
|
||||
index = i
|
||||
break
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
if index >= 0
|
||||
self.sequence_managers.remove(index)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
# Main tick function called by fast_loop
|
||||
def on_tick(current_time)
|
||||
if !self.is_running
|
||||
return false
|
||||
end
|
||||
|
||||
# Start timing this tick
|
||||
var tick_start = tasmota.millis()
|
||||
|
||||
if current_time == nil
|
||||
current_time = tasmota.millis()
|
||||
current_time = tick_start
|
||||
end
|
||||
|
||||
# Check if strip length changed since last time
|
||||
@ -231,81 +181,57 @@ class AnimationEngine
|
||||
return true
|
||||
end
|
||||
|
||||
# Update sequence managers
|
||||
var i = 0
|
||||
while i < size(self.sequence_managers)
|
||||
self.sequence_managers[i].update(current_time)
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Process any queued events (non-blocking)
|
||||
self._process_events(current_time)
|
||||
|
||||
# Update and render animations
|
||||
# Update and render root animation (which updates all children)
|
||||
# Measure animation calculation time separately
|
||||
var anim_start = tasmota.millis()
|
||||
self._update_and_render(current_time)
|
||||
var anim_end = tasmota.millis()
|
||||
var anim_duration = anim_end - anim_start
|
||||
|
||||
# End timing and record metrics
|
||||
var tick_end = tasmota.millis()
|
||||
var tick_duration = tick_end - tick_start
|
||||
self._record_tick_metrics(tick_duration, anim_duration, current_time)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Unified update and render process
|
||||
def _update_and_render(time_ms)
|
||||
var active_count = 0
|
||||
# Update root animation (which updates all children)
|
||||
self.root_animation.update(time_ms)
|
||||
|
||||
# First loop: update animations and remove completed ones in-line
|
||||
var i = 0
|
||||
while i < size(self.animations)
|
||||
var anim = self.animations[i]
|
||||
var still_running = anim.update(time_ms)
|
||||
|
||||
if still_running && anim.is_running
|
||||
# Animation is still active, keep it
|
||||
active_count += 1
|
||||
i += 1
|
||||
else
|
||||
# Animation is completed, remove it in-line
|
||||
self.animations.remove(i)
|
||||
self.render_needed = true
|
||||
# Don't increment i since we removed an element
|
||||
end
|
||||
end
|
||||
|
||||
# Skip rendering if no active animations
|
||||
if active_count == 0
|
||||
# Skip rendering if no children
|
||||
if self.root_animation.is_empty()
|
||||
if self.render_needed
|
||||
self._clear_strip()
|
||||
self.render_needed = false
|
||||
end
|
||||
return
|
||||
return 0 # Return 0 for hardware time when no rendering
|
||||
end
|
||||
|
||||
# Render active animations with efficient blending
|
||||
self._render_animations(self.animations, time_ms)
|
||||
self.render_needed = false
|
||||
end
|
||||
|
||||
# Efficient animation rendering with minimal buffer operations
|
||||
def _render_animations(animations, time_ms)
|
||||
# Clear main buffer
|
||||
self.frame_buffer.clear()
|
||||
|
||||
# Render animations in priority order (highest first)
|
||||
var i = 0
|
||||
while i < size(animations)
|
||||
var anim = animations[i]
|
||||
# Clear temp buffer and render animation
|
||||
self.temp_buffer.clear()
|
||||
var rendered = anim.render(self.temp_buffer, time_ms)
|
||||
|
||||
if rendered
|
||||
anim.post_render(self.temp_buffer, time_ms)
|
||||
# Blend temp buffer into main buffer
|
||||
self.frame_buffer.blend_pixels(self.frame_buffer.pixels, self.temp_buffer.pixels)
|
||||
end
|
||||
i += 1
|
||||
# Render root animation (which renders all children with blending)
|
||||
var rendered = self.root_animation.render(self.frame_buffer, time_ms)
|
||||
|
||||
if rendered
|
||||
# Apply root animation's post-processing (opacity, etc.)
|
||||
self.root_animation.post_render(self.frame_buffer, time_ms)
|
||||
end
|
||||
|
||||
# Output to strip
|
||||
# Measure hardware output time separately
|
||||
var hw_start = tasmota.millis()
|
||||
self._output_to_strip()
|
||||
var hw_end = tasmota.millis()
|
||||
var hw_duration = hw_end - hw_start
|
||||
|
||||
self.render_needed = false
|
||||
return hw_duration
|
||||
end
|
||||
|
||||
# Output frame buffer to LED strip
|
||||
@ -324,27 +250,6 @@ class AnimationEngine
|
||||
self.strip.show()
|
||||
end
|
||||
|
||||
# Sort animations by priority (higher first)
|
||||
def _sort_animations()
|
||||
var n = size(self.animations)
|
||||
if n <= 1
|
||||
return
|
||||
end
|
||||
|
||||
# Insertion sort for small lists
|
||||
var i = 1
|
||||
while i < n
|
||||
var key = self.animations[i]
|
||||
var j = i
|
||||
while j > 0 && self.animations[j-1].priority < key.priority
|
||||
self.animations[j] = self.animations[j-1]
|
||||
j -= 1
|
||||
end
|
||||
self.animations[j] = key
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Event processing methods
|
||||
def _process_events(current_time)
|
||||
# Process any queued events from the animation event manager
|
||||
@ -354,29 +259,172 @@ class AnimationEngine
|
||||
end
|
||||
end
|
||||
|
||||
# Interrupt current animations
|
||||
def interrupt_current()
|
||||
# Stop all currently running animations
|
||||
for anim : self.animations
|
||||
if anim.is_running
|
||||
anim.stop()
|
||||
end
|
||||
# Record tick metrics and print stats periodically
|
||||
def _record_tick_metrics(tick_duration, anim_duration, current_time)
|
||||
# Initialize stats time on first tick
|
||||
if self.last_stats_time == 0
|
||||
self.last_stats_time = current_time
|
||||
end
|
||||
|
||||
# Update streaming statistics (no array storage)
|
||||
self.tick_count += 1
|
||||
self.tick_time_sum += tick_duration
|
||||
|
||||
# Update tick min/max
|
||||
if tick_duration < self.tick_time_min
|
||||
self.tick_time_min = tick_duration
|
||||
end
|
||||
if tick_duration > self.tick_time_max
|
||||
self.tick_time_max = tick_duration
|
||||
end
|
||||
|
||||
# Update animation calculation stats
|
||||
self.anim_time_sum += anim_duration
|
||||
if anim_duration < self.anim_time_min
|
||||
self.anim_time_min = anim_duration
|
||||
end
|
||||
if anim_duration > self.anim_time_max
|
||||
self.anim_time_max = anim_duration
|
||||
end
|
||||
|
||||
# Hardware time is the difference between total and animation time
|
||||
var hw_duration = tick_duration - anim_duration
|
||||
self.hw_time_sum += hw_duration
|
||||
if hw_duration < self.hw_time_min
|
||||
self.hw_time_min = hw_duration
|
||||
end
|
||||
if hw_duration > self.hw_time_max
|
||||
self.hw_time_max = hw_duration
|
||||
end
|
||||
|
||||
# Check if it's time to print stats (every 5 seconds)
|
||||
var time_since_stats = current_time - self.last_stats_time
|
||||
if time_since_stats >= self.stats_period
|
||||
self._print_stats(time_since_stats)
|
||||
|
||||
# Reset for next period
|
||||
self.tick_count = 0
|
||||
self.tick_time_sum = 0
|
||||
self.tick_time_min = 999999
|
||||
self.tick_time_max = 0
|
||||
self.anim_time_sum = 0
|
||||
self.anim_time_min = 999999
|
||||
self.anim_time_max = 0
|
||||
self.hw_time_sum = 0
|
||||
self.hw_time_min = 999999
|
||||
self.hw_time_max = 0
|
||||
self.last_stats_time = current_time
|
||||
end
|
||||
end
|
||||
|
||||
# Interrupt all animations
|
||||
def interrupt_all()
|
||||
self.clear()
|
||||
# Print CPU statistics
|
||||
def _print_stats(period_ms)
|
||||
if self.tick_count == 0
|
||||
return
|
||||
end
|
||||
|
||||
# Calculate statistics
|
||||
var expected_ticks = period_ms / 5 # Expected ticks at 5ms intervals
|
||||
var missed_ticks = expected_ticks - self.tick_count
|
||||
|
||||
# Calculate means from sums
|
||||
var mean_time = self.tick_time_sum / self.tick_count
|
||||
var mean_anim = self.anim_time_sum / self.tick_count
|
||||
var mean_hw = self.hw_time_sum / self.tick_count
|
||||
|
||||
# Calculate CPU usage percentage
|
||||
var cpu_percent = (self.tick_time_sum * 100) / period_ms
|
||||
|
||||
# Format and log stats - split into animation calc vs hardware output
|
||||
var stats_msg = f"AnimEngine: ticks={self.tick_count}/{int(expected_ticks)} missed={int(missed_ticks)} total={mean_time:.2f}ms({self.tick_time_min}-{self.tick_time_max}) anim={mean_anim:.2f}ms({self.anim_time_min}-{self.anim_time_max}) hw={mean_hw:.2f}ms({self.hw_time_min}-{self.hw_time_max}) cpu={cpu_percent:.1f}%"
|
||||
tasmota.log(stats_msg, 3) # Log level 3 (DEBUG)
|
||||
|
||||
# Print custom profiling points if any
|
||||
self._print_profile_points()
|
||||
end
|
||||
|
||||
# Custom profiling API - start measuring a code section
|
||||
#
|
||||
# @param name: string - Name of the profiling point
|
||||
#
|
||||
# Usage:
|
||||
# engine.profile_start("my_section")
|
||||
# # ... code to measure ...
|
||||
# engine.profile_end("my_section")
|
||||
def profile_start(name)
|
||||
self.profile_start_times[name] = tasmota.millis()
|
||||
end
|
||||
|
||||
# Custom profiling API - end measuring a code section
|
||||
#
|
||||
# @param name: string - Name of the profiling point (must match profile_start)
|
||||
def profile_end(name)
|
||||
var start_time = self.profile_start_times.find(name)
|
||||
if start_time == nil
|
||||
return # No matching start
|
||||
end
|
||||
|
||||
var end_time = tasmota.millis()
|
||||
var duration = end_time - start_time
|
||||
|
||||
# Get or create stats for this profile point
|
||||
var stats = self.profile_points.find(name)
|
||||
if stats == nil
|
||||
stats = {
|
||||
'count': 0,
|
||||
'sum': 0,
|
||||
'min': 999999,
|
||||
'max': 0
|
||||
}
|
||||
self.profile_points[name] = stats
|
||||
end
|
||||
|
||||
# Update streaming statistics
|
||||
stats['count'] += 1
|
||||
stats['sum'] += duration
|
||||
if duration < stats['min']
|
||||
stats['min'] = duration
|
||||
end
|
||||
if duration > stats['max']
|
||||
stats['max'] = duration
|
||||
end
|
||||
|
||||
# Clear start time
|
||||
self.profile_start_times.remove(name)
|
||||
end
|
||||
|
||||
# Print custom profiling points statistics
|
||||
def _print_profile_points()
|
||||
if size(self.profile_points) == 0
|
||||
return
|
||||
end
|
||||
|
||||
for name: self.profile_points.keys()
|
||||
var stats = self.profile_points[name]
|
||||
if stats['count'] > 0
|
||||
var mean = stats['sum'] / stats['count']
|
||||
var msg = f" Profile[{name}]: count={stats['count']} mean={mean:.2f}ms min={stats['min']}ms max={stats['max']}ms"
|
||||
tasmota.log(msg, 3)
|
||||
end
|
||||
end
|
||||
|
||||
# Reset profile points for next period
|
||||
self.profile_points = {}
|
||||
end
|
||||
|
||||
# Interrupt current animations
|
||||
def interrupt_current()
|
||||
self.root_animation.stop()
|
||||
end
|
||||
|
||||
# Interrupt specific animation by name
|
||||
def interrupt_animation(name)
|
||||
var i = 0
|
||||
while i < size(self.animations)
|
||||
var anim = self.animations[i]
|
||||
if anim.name != nil && anim.name == name
|
||||
anim.stop(anim)
|
||||
self.animations.remove(i)
|
||||
while i < size(self.root_animation.children)
|
||||
var child = self.root_animation.children[i]
|
||||
if isinstance(child, animation.animation) && child.name != nil && child.name == name
|
||||
child.stop()
|
||||
self.root_animation.children.remove(i)
|
||||
return
|
||||
end
|
||||
i += 1
|
||||
@ -393,9 +441,7 @@ class AnimationEngine
|
||||
|
||||
# Resume after a delay (placeholder for future implementation)
|
||||
def resume_after(delay_ms)
|
||||
# For now, just resume immediately
|
||||
# Future implementation could use a timer
|
||||
self.resume()
|
||||
tasmota.set_timer(delay_ms, def () self.resume() end)
|
||||
end
|
||||
|
||||
# Utility methods for compatibility
|
||||
@ -404,7 +450,6 @@ class AnimationEngine
|
||||
end
|
||||
|
||||
def get_strip_length()
|
||||
self.check_strip_length()
|
||||
return self.width
|
||||
end
|
||||
|
||||
@ -413,11 +458,22 @@ class AnimationEngine
|
||||
end
|
||||
|
||||
def size()
|
||||
return size(self.animations)
|
||||
# Count only animations, not sequences (for backward compatibility)
|
||||
return self.root_animation.size_animations()
|
||||
end
|
||||
|
||||
def get_animations()
|
||||
return self.animations
|
||||
return self.root_animation.get_animations()
|
||||
end
|
||||
|
||||
# Backward compatibility: get sequence managers
|
||||
def sequence_managers()
|
||||
return self.root_animation.sequences
|
||||
end
|
||||
|
||||
# Backward compatibility: get animations list
|
||||
def animations()
|
||||
return self.get_animations()
|
||||
end
|
||||
|
||||
# Check if the length of the strip changes
|
||||
@ -457,23 +513,20 @@ class AnimationEngine
|
||||
self.strip = nil
|
||||
end
|
||||
|
||||
# Sequence iteration tracking methods
|
||||
# Sequence iteration tracking methods, delegate to EngineProxy
|
||||
|
||||
# Push a new iteration context onto the stack
|
||||
# Called when a sequence starts repeating
|
||||
#
|
||||
# @param iteration_number: int - The current iteration number (0-based)
|
||||
def push_iteration_context(iteration_number)
|
||||
self.iteration_stack.push(iteration_number)
|
||||
return self.root_animation.push_iteration_context(iteration_number)
|
||||
end
|
||||
|
||||
# Pop the current iteration context from the stack
|
||||
# Called when a sequence finishes repeating
|
||||
def pop_iteration_context()
|
||||
if size(self.iteration_stack) > 0
|
||||
return self.iteration_stack.pop()
|
||||
end
|
||||
return nil
|
||||
return self.root_animation.pop_iteration_context()
|
||||
end
|
||||
|
||||
# Update the current iteration number in the top context
|
||||
@ -481,9 +534,7 @@ class AnimationEngine
|
||||
#
|
||||
# @param iteration_number: int - The new iteration number (0-based)
|
||||
def update_current_iteration(iteration_number)
|
||||
if size(self.iteration_stack) > 0
|
||||
self.iteration_stack[-1] = iteration_number
|
||||
end
|
||||
return self.root_animation.update_current_iteration(iteration_number)
|
||||
end
|
||||
|
||||
# Get the current iteration number from the innermost sequence context
|
||||
@ -491,10 +542,7 @@ class AnimationEngine
|
||||
#
|
||||
# @return int|nil - Current iteration number (0-based) or nil if not in sequence
|
||||
def get_current_iteration_number()
|
||||
if size(self.iteration_stack) > 0
|
||||
return self.iteration_stack[-1]
|
||||
end
|
||||
return nil
|
||||
return self.root_animation.get_current_iteration_number()
|
||||
end
|
||||
|
||||
# String representation
|
||||
|
||||
390
lib/libesp32/berry_animation/src/core/engine_proxy.be
Normal file
390
lib/libesp32/berry_animation/src/core/engine_proxy.be
Normal file
@ -0,0 +1,390 @@
|
||||
# Engine Proxy - Combines rendering and orchestration
|
||||
#
|
||||
# An EngineProxy is a Playable that can both render visual content
|
||||
# AND orchestrate sub-animations and sequences. This enables complex
|
||||
# composite effects that combine multiple animations with timing control.
|
||||
#
|
||||
# Example use cases:
|
||||
# - An animation that renders a background while orchestrating foreground effects
|
||||
# - A composite effect that switches between different animations over time
|
||||
# - A complex pattern that combines multiple sub-animations with sequences
|
||||
|
||||
import "./core/param_encoder" as encode_constraints
|
||||
|
||||
class EngineProxy : animation.animation
|
||||
# Non-parameter instance variables
|
||||
var animations # List of child playables (animations and sequences)
|
||||
var sequences # List of child sequence managers
|
||||
|
||||
# Sequence iteration tracking (stack-based for nested sequences)
|
||||
var iteration_stack # Stack of iteration numbers for nested sequences
|
||||
|
||||
# Cached time for child access (updated during update())
|
||||
var time_ms # Current time in milliseconds (cached from engine)
|
||||
|
||||
# Parameter definitions (extends Animation's PARAMS)
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Inherited from Animation: name, is_running, priority, duration, loop, opacity, color
|
||||
# EngineProxy has no additional parameters beyond Animation
|
||||
})
|
||||
|
||||
def init(engine)
|
||||
# Initialize parameter system with engine
|
||||
super(self).init(engine)
|
||||
|
||||
# Initialize non-parameter instance variables
|
||||
self.animations = []
|
||||
self.sequences = []
|
||||
|
||||
# Initialize iteration tracking stack
|
||||
self.iteration_stack = []
|
||||
|
||||
# Initialize time cache
|
||||
self.time_ms = 0
|
||||
|
||||
# Call template setup method (empty placeholder for subclasses)
|
||||
self.setup_template()
|
||||
end
|
||||
|
||||
# Template setup method - empty placeholder for template animations
|
||||
# Template animations override this method to set up their animations and sequences
|
||||
def setup_template()
|
||||
# Empty placeholder - template animations override this method
|
||||
end
|
||||
|
||||
# Is empty
|
||||
#
|
||||
# @return true both animations and sequences are empty
|
||||
def is_empty()
|
||||
return (size(self.animations) == 0) && (size(self.sequences) == 0)
|
||||
end
|
||||
|
||||
# Number of animations
|
||||
#
|
||||
# @return true both animations and sequences are empty
|
||||
def size_animations()
|
||||
return size(self.animations)
|
||||
end
|
||||
|
||||
def get_animations()
|
||||
# Return only Animation children (not SequenceManagers)
|
||||
var anims = []
|
||||
for child : self.animations
|
||||
if isinstance(child, animation.animation)
|
||||
anims.push(child)
|
||||
end
|
||||
end
|
||||
return anims
|
||||
end
|
||||
|
||||
# Add a child playable (animation or sequence)
|
||||
#
|
||||
# @param child: Playable - The child to add
|
||||
# @return self for method chaining
|
||||
def add(obj)
|
||||
if isinstance(obj, animation.sequence_manager)
|
||||
return self._add_sequence_manager(obj)
|
||||
# Check if it's an Animation (or subclass)
|
||||
elif isinstance(obj, animation.animation)
|
||||
return self._add_animation(obj)
|
||||
else
|
||||
# Unknown type - provide helpful error message
|
||||
raise "type_error", "only Animation or SequenceManager"
|
||||
end
|
||||
end
|
||||
|
||||
# Add a sequence manager
|
||||
def _add_sequence_manager(sequence_manager)
|
||||
if (self.sequences.find(sequence_manager) == nil)
|
||||
self.sequences.push(sequence_manager)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Add an animation with automatic priority sorting
|
||||
#
|
||||
# @param anim: animation - The animation instance to add (if not already listed)
|
||||
# @return true if succesful (TODO always true)
|
||||
def _add_animation(anim)
|
||||
if (self.animations.find(anim) == nil) # not already in list
|
||||
# Add and sort by priority (higher priority first)
|
||||
self.animations.push(anim)
|
||||
self._sort_animations_by_priority()
|
||||
# If the engine is already started, auto-start the animation
|
||||
if self.is_running
|
||||
anim.start(self.engine.time_ms)
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Sort animations by priority (animations only, sequences don't have priority)
|
||||
# Higher priority animations render on top
|
||||
def _sort_animations_by_priority()
|
||||
var n = size(self.animations)
|
||||
if n <= 1
|
||||
return
|
||||
end
|
||||
|
||||
# Insertion sort for small lists
|
||||
# Only sort animations (not sequences), keep sequences at end
|
||||
var i = 1
|
||||
while i < n
|
||||
var key = self.animations[i]
|
||||
|
||||
# Skip if key is not an animation
|
||||
if !isinstance(key, animation.animation)
|
||||
i += 1
|
||||
continue
|
||||
end
|
||||
|
||||
var j = i
|
||||
while j > 0
|
||||
var prev = self.animations[j-1]
|
||||
# Stop if previous is not an animation or has higher/equal priority
|
||||
if !isinstance(prev, animation.animation) || prev.priority >= key.priority # todo is test still useful?
|
||||
break
|
||||
end
|
||||
self.animations[j] = self.animations[j-1]
|
||||
j -= 1
|
||||
end
|
||||
self.animations[j] = key
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Remove a child playable
|
||||
#
|
||||
# @param child: Playable - The child to remove
|
||||
# @return true if actually removed
|
||||
def _remove_animation(obj)
|
||||
var idx = self.animations.find(obj)
|
||||
if idx != nil
|
||||
self.animations.remove(idx)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Remove a sequence manager
|
||||
#
|
||||
# @param obj: Sequence Manager instance
|
||||
# @return true if actually removed
|
||||
def _remove_sequence_manager(obj)
|
||||
var idx = self.sequences.find(obj)
|
||||
if idx != nil
|
||||
self.sequences.remove(idx)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Generic remove method that delegates to specific remove methods
|
||||
# @param obj: Animation or SequenceManager - The object to remove
|
||||
# @return self for method chaining
|
||||
def remove(obj)
|
||||
# Check if it's a SequenceManager
|
||||
if isinstance(obj, animation.sequence_manager)
|
||||
return self._remove_sequence_manager(obj)
|
||||
# Check if it's an Animation (or subclass)
|
||||
elif isinstance(obj, animation.animation)
|
||||
return self._remove_animation(obj)
|
||||
else
|
||||
# Unknown type - ignore
|
||||
end
|
||||
end
|
||||
|
||||
# Start the hybrid animation and all its animations
|
||||
#
|
||||
# @param time_ms: int - Start time in milliseconds
|
||||
# @return self for method chaining
|
||||
def start(time_ms)
|
||||
# Call parent start
|
||||
super(self).start(time_ms)
|
||||
|
||||
# Start all sequences
|
||||
var idx = 0
|
||||
while idx < size(self.sequences)
|
||||
self.sequences[idx].start(time_ms)
|
||||
idx += 1
|
||||
end
|
||||
|
||||
# Start all animations
|
||||
idx = 0
|
||||
while idx < size(self.animations)
|
||||
self.animations[idx].start(time_ms)
|
||||
idx += 1
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
# Stop the hybrid animation and all its animations
|
||||
#
|
||||
# @return self for method chaining
|
||||
def stop()
|
||||
# Stop all sequences
|
||||
var idx = 0
|
||||
while idx < size(self.sequences)
|
||||
self.sequences[idx].stop()
|
||||
idx += 1
|
||||
end
|
||||
|
||||
# Stop all animations
|
||||
idx = 0
|
||||
while idx < size(self.animations)
|
||||
self.animations[idx].stop()
|
||||
idx += 1
|
||||
end
|
||||
|
||||
# Call parent stop
|
||||
super(self).stop()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
# Stop and clear the hybrid animation and all its animations
|
||||
#
|
||||
# @return self for method chaining
|
||||
def clear()
|
||||
self.stop()
|
||||
self.animations = []
|
||||
self.sequences = []
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
# Update the hybrid animation and all its animations
|
||||
#
|
||||
# @param time_ms: int - Current time in milliseconds
|
||||
# @return bool - True if still running, false if completed
|
||||
def update(time_ms)
|
||||
# Cache time for child access
|
||||
self.time_ms = time_ms
|
||||
|
||||
# Update parent animation state
|
||||
var still_running = super(self).update(time_ms)
|
||||
|
||||
if !still_running
|
||||
return false
|
||||
end
|
||||
|
||||
# Update all child sequences
|
||||
for seq : self.sequences
|
||||
seq.update(time_ms)
|
||||
end
|
||||
|
||||
# Update all child animations (sequences are also in animations list)
|
||||
for child : self.animations
|
||||
if isinstance(child, animation.animation)
|
||||
child.update(time_ms)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Render the hybrid animation
|
||||
# Renders own content first, then all child animations
|
||||
#
|
||||
# @param frame: FrameBuffer - The frame buffer to render to
|
||||
# @param time_ms: int - Current time in milliseconds
|
||||
# @return bool - True if frame was modified, false otherwise
|
||||
def render(frame, time_ms)
|
||||
if !self.is_running || frame == nil
|
||||
return false
|
||||
end
|
||||
|
||||
# # update sequences first
|
||||
# var i = 0
|
||||
# while i < size(self.sequences)
|
||||
# self.sequences[i].update(time_ms)
|
||||
# i += 1
|
||||
# end
|
||||
|
||||
var modified = false
|
||||
|
||||
# Render own content (base Animation implementation)
|
||||
modified = super(self).render(frame, time_ms)
|
||||
|
||||
# Render all child animations (but not sequences - they don't render)
|
||||
for child : self.animations
|
||||
if isinstance(child, animation.animation) && child.is_running
|
||||
# Create temp buffer for child
|
||||
var temp_frame = animation.frame_buffer(frame.width)
|
||||
var child_rendered = child.render(temp_frame, time_ms)
|
||||
|
||||
if child_rendered
|
||||
# Apply child's post-processing
|
||||
child.post_render(temp_frame, time_ms)
|
||||
|
||||
# Blend child into main frame
|
||||
frame.blend_pixels(frame.pixels, temp_frame.pixels)
|
||||
modified = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return modified
|
||||
end
|
||||
|
||||
# Delegation methods to engine (for compatibility with child objects)
|
||||
|
||||
# Get strip length from engine
|
||||
def get_strip_length()
|
||||
return (self.engine != nil) ? self.engine.get_strip_length() : 0
|
||||
end
|
||||
|
||||
# Sequence iteration tracking methods
|
||||
|
||||
# Push a new iteration context onto the stack
|
||||
# Called when a sequence starts repeating
|
||||
#
|
||||
# @param iteration_number: int - The current iteration number (0-based)
|
||||
def push_iteration_context(iteration_number)
|
||||
self.iteration_stack.push(iteration_number)
|
||||
end
|
||||
|
||||
# Pop the current iteration context from the stack
|
||||
# Called when a sequence finishes repeating
|
||||
def pop_iteration_context()
|
||||
if size(self.iteration_stack) > 0
|
||||
return self.iteration_stack.pop()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
# Update the current iteration number in the top context
|
||||
# Called when a sequence advances to the next iteration
|
||||
#
|
||||
# @param iteration_number: int - The new iteration number (0-based)
|
||||
def update_current_iteration(iteration_number)
|
||||
if size(self.iteration_stack) > 0
|
||||
self.iteration_stack[-1] = iteration_number
|
||||
end
|
||||
end
|
||||
|
||||
# Get the current iteration number from the innermost sequence context
|
||||
# Used by IterationNumberProvider to return the current iteration
|
||||
#
|
||||
# @return int|nil - Current iteration number (0-based) or nil if not in sequence
|
||||
def get_current_iteration_number()
|
||||
if size(self.iteration_stack) > 0
|
||||
return self.iteration_stack[-1]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
# String representation
|
||||
def tostring()
|
||||
return f"{classname(self)}({self.name}, animations={size(self.animations)}, sequences={size(self.sequences)}, running={self.is_running})"
|
||||
end
|
||||
end
|
||||
|
||||
return {'engine_proxy': EngineProxy}
|
||||
@ -48,7 +48,7 @@
|
||||
# @return map - Map of parameter names to encoded bytes() objects
|
||||
#
|
||||
# Example:
|
||||
# encode_constraints({"color": {"default": 0xFFFFFFFF}, "size": {"min": 0, "max": 255, "default": 128}})
|
||||
# animation.enc_params({"color": {"default": 0xFFFFFFFF}, "size": {"min": 0, "max": 255, "default": 128}})
|
||||
# => {"color": bytes("04 02 FFFFFFFF"), "size": bytes("07 00 00 FF 80")}
|
||||
def encode_constraints(params_map)
|
||||
# Nested function: Encode a single constraint map into bytes() format
|
||||
@ -281,4 +281,6 @@ end
|
||||
# Export only the encode function (decode not needed - use constraint_mask/constraint_find instead)
|
||||
# Note: constraint_mask() and constraint_find() are static methods
|
||||
# in ParameterizedObject class for accessing encoded constraints
|
||||
return encode_constraints
|
||||
return {
|
||||
'enc_params': encode_constraints
|
||||
}
|
||||
|
||||
@ -16,9 +16,9 @@ class ParameterizedObject
|
||||
var start_time # Time when object started (ms) (int), value is set at first call to update() or render()
|
||||
|
||||
# Static parameter definitions - should be overridden by subclasses
|
||||
static var PARAMS = encode_constraints(
|
||||
{"is_running": {"type": "bool", "default": false}
|
||||
}) # Whether the object is active
|
||||
static var PARAMS = animation.enc_params(
|
||||
{"is_running": {"type": "bool", "default": false} # Whether the object is active
|
||||
})
|
||||
|
||||
# Initialize parameter system
|
||||
#
|
||||
@ -175,13 +175,8 @@ class ParameterizedObject
|
||||
|
||||
var value = self.values[name]
|
||||
|
||||
# If it's a ValueProvider, resolve it using produce_value
|
||||
if animation.is_value_provider(value)
|
||||
return value.produce_value(name, time_ms)
|
||||
else
|
||||
# It's a static value, return as-is
|
||||
return value
|
||||
end
|
||||
# Apply produce_value() if it' a ValueProvider
|
||||
return self.resolve_value(value, name, time_ms)
|
||||
end
|
||||
|
||||
# Validate a parameter value against its constraints
|
||||
@ -210,7 +205,7 @@ class ParameterizedObject
|
||||
|
||||
# Check if there's a default value (nil is acceptable if there's a default)
|
||||
if self.constraint_mask(encoded_constraints, "default")
|
||||
return value # nil is acceptable, will use default
|
||||
return self.constraint_find(encoded_constraints, "default") # nil is not allowed, use default
|
||||
end
|
||||
|
||||
# nil is not allowed for this parameter
|
||||
@ -220,6 +215,15 @@ class ParameterizedObject
|
||||
# Type validation - default type is "int" if not specified
|
||||
var expected_type = self.constraint_find(encoded_constraints, "type", "int")
|
||||
|
||||
# Normalize type synonyms to their base types
|
||||
# 'time', 'percentage', 'color' are synonyms for 'int'
|
||||
# 'palette' is synonym for 'bytes'
|
||||
if expected_type == "time" || expected_type == "percentage" || expected_type == "color"
|
||||
expected_type = "int"
|
||||
elif expected_type == "palette"
|
||||
expected_type = "bytes"
|
||||
end
|
||||
|
||||
# Get actual type for validation
|
||||
var actual_type = type(value)
|
||||
|
||||
@ -326,9 +330,20 @@ class ParameterizedObject
|
||||
# @param param_name: string - Parameter name for specific produce_value() method lookup
|
||||
# @param time_ms: int - Current time in milliseconds
|
||||
# @return any - The resolved value (static or from provider)
|
||||
def resolve_value(value, param_name, time_ms)
|
||||
def resolve_value(value, name, time_ms)
|
||||
if animation.is_value_provider(value) # this also captures 'nil'
|
||||
return value.produce_value(param_name, time_ms)
|
||||
var ret = value.produce_value(name, time_ms)
|
||||
|
||||
# If result is `nil` we check if the parameter is nillable, if so use default value
|
||||
if (ret == nil)
|
||||
var encoded_constraints = self._get_param_def(name)
|
||||
if !self.constraint_mask(encoded_constraints, "nillable") &&
|
||||
self.constraint_mask(encoded_constraints, "default")
|
||||
|
||||
ret = self.constraint_find(encoded_constraints, "default")
|
||||
end
|
||||
end
|
||||
return ret
|
||||
else
|
||||
return value
|
||||
end
|
||||
@ -416,6 +431,15 @@ class ParameterizedObject
|
||||
return introspect.toptr(self) == introspect.toptr(other)
|
||||
end
|
||||
|
||||
# Default method to convert instance to boolean
|
||||
# Having an explicit method prevents from calling member()
|
||||
# Always return 'true' to mimick default test of instance existance
|
||||
#
|
||||
# @return bool - always True since the instance is not 'nil'
|
||||
def tobool()
|
||||
return true
|
||||
end
|
||||
|
||||
# Inequality operator for object identity comparison
|
||||
# This prevents the member() method from being called during != comparisons
|
||||
#
|
||||
@ -495,7 +519,7 @@ class ParameterizedObject
|
||||
#
|
||||
# Encoding constraints (see param_encoder.be):
|
||||
# import param_encoder
|
||||
# var encoded = param_encoder.encode_constraints({"min": 0, "max": 255, "default": 128})
|
||||
# var encoded = param_encoder.animation.enc_params({"min": 0, "max": 255, "default": 128})
|
||||
#
|
||||
# Checking if constraint contains a field:
|
||||
# if ParameterizedObject.constraint_mask(encoded, "min")
|
||||
@ -538,7 +562,7 @@ class ParameterizedObject
|
||||
"function" # 0x06
|
||||
]
|
||||
static def constraint_mask(encoded_bytes, name)
|
||||
if size(encoded_bytes) > 0
|
||||
if (encoded_bytes != nil) && size(encoded_bytes) > 0
|
||||
var index_mask = _class._MASK.find(name)
|
||||
if (index_mask != nil)
|
||||
return (encoded_bytes[0] & (1 << index_mask))
|
||||
|
||||
73
lib/libesp32/berry_animation/src/core/playable_base.be
Normal file
73
lib/libesp32/berry_animation/src/core/playable_base.be
Normal file
@ -0,0 +1,73 @@
|
||||
# Playable Base Class - Common interface for animations and sequences
|
||||
#
|
||||
# A Playable is anything that can be started, stopped, and updated over time.
|
||||
# This serves as the common base class for both Animation (visual rendering)
|
||||
# and SequenceManager (orchestration), allowing the engine to treat them uniformly.
|
||||
#
|
||||
# This enables:
|
||||
# - Unified engine management (single list instead of separate lists)
|
||||
# - Hybrid objects that combine rendering and orchestration
|
||||
# - Consistent lifecycle management (start/stop/update)
|
||||
|
||||
import "./core/param_encoder" as encode_constraints
|
||||
|
||||
class Playable : animation.parameterized_object
|
||||
# Parameter definitions - minimal shared interface
|
||||
static var PARAMS = animation.enc_params({
|
||||
})
|
||||
|
||||
# Initialize a new playable
|
||||
#
|
||||
# @param engine: AnimationEngine - Reference to the animation engine (required)
|
||||
def init(engine)
|
||||
# Initialize parameter system with engine
|
||||
super(self).init(engine)
|
||||
end
|
||||
|
||||
# Start the playable at a specific time
|
||||
# Subclasses should override this to implement their start behavior
|
||||
#
|
||||
# @param time_ms: int - Start time in milliseconds (optional, uses engine time if nil)
|
||||
# @return self for method chaining
|
||||
def start(time_ms)
|
||||
# Use engine time if not provided
|
||||
if time_ms == nil
|
||||
time_ms = self.engine.time_ms
|
||||
end
|
||||
|
||||
# Set is_running to true
|
||||
self.values["is_running"] = true
|
||||
|
||||
# Always update start_time when start() is called (restart behavior)
|
||||
self.start_time = time_ms
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
# Stop the playable
|
||||
# Subclasses should override this to implement their stop behavior
|
||||
#
|
||||
# @return self for method chaining
|
||||
def stop()
|
||||
# Set is_running to false
|
||||
self.values["is_running"] = false
|
||||
return self
|
||||
end
|
||||
|
||||
# Update playable state based on current time
|
||||
# Subclasses must override this to implement their update logic
|
||||
#
|
||||
# @param time_ms: int - Current time in milliseconds
|
||||
# @return bool - True if playable is still running, false if completed
|
||||
def update(time_ms)
|
||||
# Default implementation just returns running state
|
||||
return self.is_running
|
||||
end
|
||||
|
||||
# String representation of the playable
|
||||
def tostring()
|
||||
return f"Playable(running={self.is_running})"
|
||||
end
|
||||
end
|
||||
|
||||
return {'playable': Playable}
|
||||
@ -1,29 +1,41 @@
|
||||
# Sequence Manager for Animation DSL
|
||||
# Handles async execution of animation sequences without blocking delays
|
||||
# Supports sub-sequences and repeat logic through recursive composition
|
||||
#
|
||||
# Extends Playable to provide the common interface for lifecycle management,
|
||||
# allowing sequences to be treated uniformly with animations by the engine.
|
||||
|
||||
class SequenceManager
|
||||
var engine # Animation engine reference
|
||||
import "./core/param_encoder" as encode_constraints
|
||||
|
||||
class SequenceManager : animation.playable
|
||||
# Non-parameter instance variables
|
||||
var active_sequence # Currently running sequence
|
||||
var sequence_state # Current sequence execution state
|
||||
var step_index # Current step in the sequence
|
||||
var step_start_time # When current step started
|
||||
var steps # List of sequence steps
|
||||
var is_running # Whether sequence is active
|
||||
|
||||
# Repeat-specific properties
|
||||
var repeat_count # Number of times to repeat this sequence (-1 for forever, 0 for no repeat)
|
||||
var current_iteration # Current iteration (0-based)
|
||||
var is_repeat_sequence # Whether this is a repeat sub-sequence
|
||||
|
||||
# Parameter definitions (extends Playable's PARAMS)
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Inherited from Playable: is_running
|
||||
# SequenceManager has no additional parameters beyond Playable
|
||||
})
|
||||
|
||||
def init(engine, repeat_count)
|
||||
self.engine = engine
|
||||
# Initialize parameter system with engine
|
||||
super(self).init(engine)
|
||||
|
||||
# Initialize non-parameter instance variables
|
||||
self.active_sequence = nil
|
||||
self.sequence_state = {}
|
||||
self.step_index = 0
|
||||
self.step_start_time = 0
|
||||
self.steps = []
|
||||
self.is_running = false
|
||||
|
||||
# Repeat logic
|
||||
self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once (can be function or number)
|
||||
@ -79,7 +91,7 @@ class SequenceManager
|
||||
def start(time_ms)
|
||||
# Stop any current sequence
|
||||
if self.is_running
|
||||
self.is_running = false
|
||||
self.values["is_running"] = false
|
||||
# Stop any sub-sequences
|
||||
self.stop_all_subsequences()
|
||||
end
|
||||
@ -88,7 +100,20 @@ class SequenceManager
|
||||
self.step_index = 0
|
||||
self.step_start_time = time_ms
|
||||
self.current_iteration = 0
|
||||
self.is_running = true
|
||||
self.values["is_running"] = true
|
||||
|
||||
# Initialize start_time if not already set
|
||||
if self.start_time == nil
|
||||
self.start_time = time_ms
|
||||
end
|
||||
|
||||
# FIXED: Check repeat count BEFORE starting execution
|
||||
# If repeat_count is 0, don't execute at all
|
||||
var resolved_repeat_count = self.get_resolved_repeat_count()
|
||||
if resolved_repeat_count == 0
|
||||
self.values["is_running"] = false
|
||||
return self
|
||||
end
|
||||
|
||||
# Push iteration context to engine stack if this is a repeat sequence
|
||||
if self.is_repeat_sequence
|
||||
@ -123,7 +148,7 @@ class SequenceManager
|
||||
# Stop this sequence manager
|
||||
def stop()
|
||||
if self.is_running
|
||||
self.is_running = false
|
||||
self.values["is_running"] = false
|
||||
|
||||
# Pop iteration context from engine stack if this is a repeat sequence
|
||||
if self.is_repeat_sequence
|
||||
@ -182,9 +207,21 @@ class SequenceManager
|
||||
self.execute_closure_steps_batch(current_time)
|
||||
else
|
||||
# Handle regular steps with duration
|
||||
if current_step.contains("duration") && current_step["duration"] > 0
|
||||
var elapsed = current_time - self.step_start_time
|
||||
if elapsed >= current_step["duration"]
|
||||
if current_step.contains("duration") && current_step["duration"] != nil
|
||||
# Resolve duration - it can be a number or a closure
|
||||
var duration_value = current_step["duration"]
|
||||
if type(duration_value) == "function"
|
||||
# Duration is a closure - call it to get the actual value
|
||||
duration_value = duration_value(self.engine)
|
||||
end
|
||||
|
||||
if duration_value > 0
|
||||
var elapsed = current_time - self.step_start_time
|
||||
if elapsed >= duration_value
|
||||
self.advance_to_next_step(current_time)
|
||||
end
|
||||
else
|
||||
# Duration is 0 or nil - complete immediately
|
||||
self.advance_to_next_step(current_time)
|
||||
end
|
||||
else
|
||||
@ -207,6 +244,12 @@ class SequenceManager
|
||||
|
||||
if step["type"] == "play"
|
||||
var anim = step["animation"]
|
||||
|
||||
# Check if animation is nil (safety check)
|
||||
if anim == nil
|
||||
return
|
||||
end
|
||||
|
||||
# Check if animation is already in the engine (avoid duplicate adds)
|
||||
var animations = self.engine.get_animations()
|
||||
var already_added = false
|
||||
@ -390,7 +433,7 @@ class SequenceManager
|
||||
end
|
||||
else
|
||||
# All iterations complete
|
||||
self.is_running = false
|
||||
self.values["is_running"] = false
|
||||
|
||||
# Pop iteration context from engine stack if this is a repeat sequence
|
||||
if self.is_repeat_sequence
|
||||
@ -400,12 +443,17 @@ class SequenceManager
|
||||
end
|
||||
|
||||
# Resolve repeat count (handle both functions and numbers)
|
||||
# Converts booleans to integers: true -> 1, false -> 0
|
||||
def get_resolved_repeat_count()
|
||||
var count = nil
|
||||
if type(self.repeat_count) == "function"
|
||||
return self.repeat_count(self.engine)
|
||||
count = self.repeat_count(self.engine)
|
||||
else
|
||||
return self.repeat_count
|
||||
count = self.repeat_count
|
||||
end
|
||||
|
||||
# Convert to integer (handles booleans: true -> 1, false -> 0)
|
||||
return int(count)
|
||||
end
|
||||
|
||||
# Check if sequence is running
|
||||
@ -413,6 +461,20 @@ class SequenceManager
|
||||
return self.is_running
|
||||
end
|
||||
|
||||
# String representation of the sequence manager
|
||||
def tostring()
|
||||
var repeat_str = ""
|
||||
if self.is_repeat_sequence
|
||||
var resolved_count = self.get_resolved_repeat_count()
|
||||
if resolved_count == -1
|
||||
repeat_str = f", repeat=forever, iter={self.current_iteration}"
|
||||
else
|
||||
repeat_str = f", repeat={resolved_count}, iter={self.current_iteration}"
|
||||
end
|
||||
end
|
||||
return f"SequenceManager(steps={size(self.steps)}, current={self.step_index}, running={self.is_running}{repeat_str})"
|
||||
end
|
||||
|
||||
# # Get current step info for debugging
|
||||
# def get_current_step_info()
|
||||
# if !self.is_running || self.step_index >= size(self.steps)
|
||||
@ -431,4 +493,4 @@ class SequenceManager
|
||||
# end
|
||||
end
|
||||
|
||||
return {'SequenceManager': SequenceManager}
|
||||
return {'sequence_manager': SequenceManager }
|
||||
|
||||
@ -12,6 +12,7 @@ class SimpleDSLTranspiler
|
||||
var symbol_table # Enhanced symbol cache: name -> {type, instance, class_obj}
|
||||
var indent_level # Track current indentation level for nested sequences
|
||||
var has_template_calls # Track if we have template calls to trigger engine.run()
|
||||
var template_animation_params # Set of parameter names when processing template animation body
|
||||
|
||||
# Context constants for process_value calls
|
||||
static var CONTEXT_VARIABLE = 1
|
||||
@ -164,6 +165,7 @@ class SimpleDSLTranspiler
|
||||
self.symbol_table = animation_dsl._symbol_table() # Enhanced symbol cache with built-in detection
|
||||
self.indent_level = 0 # Track current indentation level
|
||||
self.has_template_calls = false # Track if we have template calls
|
||||
self.template_animation_params = nil # Set of parameter names when processing template animation body
|
||||
|
||||
# Note: Special functions like 'log' are now auto-discovered dynamically by the symbol table
|
||||
end
|
||||
@ -405,6 +407,47 @@ class SimpleDSLTranspiler
|
||||
end
|
||||
end
|
||||
|
||||
# Transpile template animation body (for engine_proxy classes)
|
||||
# Similar to template body but uses self.add() instead of engine.add()
|
||||
def transpile_template_animation_body()
|
||||
try
|
||||
# Process all statements in template animation body until we hit the closing brace
|
||||
var brace_depth = 0
|
||||
while !self.at_end()
|
||||
var tok = self.current()
|
||||
|
||||
# Check for template end condition
|
||||
if tok != nil && tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-# && brace_depth == 0
|
||||
# This is the closing brace of the template - stop processing
|
||||
break
|
||||
end
|
||||
|
||||
# Track brace depth for nested braces
|
||||
if tok != nil && tok.type == 26 #-animation_dsl.Token.LEFT_BRACE-#
|
||||
brace_depth += 1
|
||||
elif tok != nil && tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-#
|
||||
brace_depth -= 1
|
||||
end
|
||||
|
||||
self.process_statement()
|
||||
end
|
||||
|
||||
# For template animations, use self.add() instead of engine.add()
|
||||
if size(self.run_statements) > 0
|
||||
for run_stmt : self.run_statements
|
||||
var obj_name = run_stmt["name"]
|
||||
var comment = run_stmt["comment"]
|
||||
# In template animations, use self.add() for engine_proxy
|
||||
self.add(f"self.add({obj_name}_){comment}")
|
||||
end
|
||||
end
|
||||
|
||||
return self.join_output()
|
||||
except .. as e, msg
|
||||
self.error(f"Template animation body transpilation failed: {msg}")
|
||||
end
|
||||
end
|
||||
|
||||
# Process statements - simplified approach
|
||||
def process_statement()
|
||||
var tok = self.current()
|
||||
@ -433,7 +476,13 @@ class SimpleDSLTranspiler
|
||||
self.skip_statement()
|
||||
return
|
||||
elif tok.value == "template"
|
||||
self.process_template()
|
||||
# Check if this is "template animation" or just "template"
|
||||
var next_tok = self.peek()
|
||||
if next_tok != nil && next_tok.type == 0 #-animation_dsl.Token.KEYWORD-# && next_tok.value == "animation"
|
||||
self.process_template_animation()
|
||||
else
|
||||
self.process_template()
|
||||
end
|
||||
else
|
||||
# For any other statement, ensure strip is initialized
|
||||
if !self.strip_initialized
|
||||
@ -787,8 +836,14 @@ class SimpleDSLTranspiler
|
||||
return
|
||||
end
|
||||
|
||||
# Generate the base function call immediately
|
||||
self.add(f"var {name}_ = animation.{func_name}(engine){inline_comment}")
|
||||
# Check if this is a template animation (user-defined, not built-in)
|
||||
if entry.is_builtin
|
||||
# Built-in animation constructor from animation module
|
||||
self.add(f"var {name}_ = animation.{func_name}(engine){inline_comment}")
|
||||
else
|
||||
# Template animation constructor (user-defined class)
|
||||
self.add(f"var {name}_ = {func_name}_animation(engine){inline_comment}")
|
||||
end
|
||||
|
||||
# Track this symbol in our symbol table
|
||||
var instance = self._create_instance_for_validation(func_name)
|
||||
@ -871,8 +926,8 @@ class SimpleDSLTranspiler
|
||||
self.next() # skip 'param'
|
||||
var param_name = self.expect_identifier()
|
||||
|
||||
# Validate parameter name
|
||||
if !self._validate_template_parameter_name(param_name, param_names_seen)
|
||||
# Validate parameter name (not a template animation)
|
||||
if !self._validate_template_parameter_name(param_name, param_names_seen, false)
|
||||
self.skip_statement()
|
||||
return
|
||||
end
|
||||
@ -918,6 +973,81 @@ class SimpleDSLTranspiler
|
||||
self.symbol_table.create_template(name, template_info)
|
||||
end
|
||||
|
||||
# Process template animation definition: template animation name { param ... }
|
||||
# Generates a class extending engine_proxy instead of a function
|
||||
def process_template_animation()
|
||||
self.next() # skip 'template'
|
||||
self.next() # skip 'animation'
|
||||
var name = self.expect_identifier()
|
||||
|
||||
# Validate that the template animation name is not reserved
|
||||
if !self.validate_user_name(name, "template animation")
|
||||
self.skip_statement()
|
||||
return
|
||||
end
|
||||
|
||||
self.expect_left_brace()
|
||||
|
||||
# First pass: collect all parameters with validation
|
||||
var params = []
|
||||
var param_types = {}
|
||||
var param_names_seen = {} # Track duplicate parameter names
|
||||
|
||||
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 == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == "param"
|
||||
# Process parameter declaration in template animation
|
||||
self.next() # skip 'param'
|
||||
var param_name = self.expect_identifier()
|
||||
|
||||
# Validate parameter name (this is a template animation)
|
||||
if !self._validate_template_parameter_name(param_name, param_names_seen, true)
|
||||
self.skip_statement()
|
||||
return
|
||||
end
|
||||
|
||||
# Parse parameter constraints (type, min, max, default)
|
||||
var param_constraints = self._parse_parameter_constraints()
|
||||
|
||||
# Add parameter to collections
|
||||
params.push(param_name)
|
||||
param_names_seen[param_name] = true
|
||||
if param_constraints != nil && size(param_constraints) > 0
|
||||
param_types[param_name] = param_constraints
|
||||
end
|
||||
|
||||
# Skip optional newline after parameter
|
||||
if self.current() != nil && self.current().type == 35 #-animation_dsl.Token.NEWLINE-#
|
||||
self.next()
|
||||
end
|
||||
else
|
||||
# Found non-param statement, break to collect body
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# Generate Berry class for this template animation
|
||||
self.generate_template_animation_class(name, params, param_types)
|
||||
|
||||
# Add template animation to symbol table with parameter information
|
||||
var template_info = {
|
||||
"params": params,
|
||||
"param_types": param_types
|
||||
}
|
||||
self.symbol_table.create_template(name, template_info)
|
||||
|
||||
# Also register as an animation constructor so it can be used like: animation x = template_name(...)
|
||||
# We create a special entry that tracks it as both a template and an animation constructor
|
||||
self._register_template_animation_constructor(name, params, param_types)
|
||||
end
|
||||
|
||||
# Process sequence definition: sequence demo { ... } or sequence demo repeat N times { ... }
|
||||
def process_sequence()
|
||||
self.next() # skip 'sequence'
|
||||
@ -971,7 +1101,7 @@ class SimpleDSLTranspiler
|
||||
if is_repeat_syntax
|
||||
# Second syntax: sequence name repeat N times { ... }
|
||||
# Create a single SequenceManager with fluent interface
|
||||
self.add(f"var {name}_ = animation.SequenceManager(engine, {repeat_count})")
|
||||
self.add(f"var {name}_ = animation.sequence_manager(engine, {repeat_count})")
|
||||
|
||||
# Process sequence body - add steps using fluent interface
|
||||
while !self.at_end() && !self.check_right_brace()
|
||||
@ -980,7 +1110,7 @@ class SimpleDSLTranspiler
|
||||
else
|
||||
# First syntax: sequence demo { ... }
|
||||
# Use fluent interface for regular sequences too (no repeat count = default)
|
||||
self.add(f"var {name}_ = animation.SequenceManager(engine)")
|
||||
self.add(f"var {name}_ = animation.sequence_manager(engine)")
|
||||
|
||||
# Process sequence body - add steps using fluent interface
|
||||
while !self.at_end() && !self.check_right_brace()
|
||||
@ -1127,7 +1257,26 @@ class SimpleDSLTranspiler
|
||||
var duration = "nil"
|
||||
if self.current() != nil && self.current().type == 0 #-animation_dsl.Token.KEYWORD-# && self.current().value == "for"
|
||||
self.next() # skip 'for'
|
||||
duration = self.process_time_value()
|
||||
var tok = self.current()
|
||||
|
||||
# Check if duration is a literal time value or a variable reference
|
||||
if tok != nil && (tok.type == 5 #-animation_dsl.Token.TIME-# || tok.type == 2 #-animation_dsl.Token.NUMBER-#)
|
||||
# Literal time value - use directly
|
||||
duration = self.process_time_value()
|
||||
elif tok != nil && tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#
|
||||
# Variable reference - need to wrap in closure for dynamic values
|
||||
var duration_expr = self.process_time_value()
|
||||
# Check if this is a template animation parameter (starts with "self.")
|
||||
if duration_expr[0..4] == "self."
|
||||
# Template animation parameter - wrap in closure for dynamic evaluation
|
||||
duration = f"def (engine) return {duration_expr} end"
|
||||
else
|
||||
# Regular variable - use directly (static value)
|
||||
duration = duration_expr
|
||||
end
|
||||
else
|
||||
duration = self.process_time_value()
|
||||
end
|
||||
end
|
||||
|
||||
var inline_comment = self.collect_inline_comment()
|
||||
@ -1221,7 +1370,7 @@ class SimpleDSLTranspiler
|
||||
self.expect_left_brace()
|
||||
|
||||
# Create a nested sub-sequence using recursive processing
|
||||
self.add(f"{self.get_indent()}.push_repeat_subsequence(animation.SequenceManager(engine, {repeat_count})")
|
||||
self.add(f"{self.get_indent()}.push_repeat_subsequence(animation.sequence_manager(engine, {repeat_count})")
|
||||
|
||||
# Increase indentation level for nested content
|
||||
self.indent_level += 1
|
||||
@ -1636,6 +1785,17 @@ class SimpleDSLTranspiler
|
||||
# Identifier - could be color, animation, variable, or object property reference
|
||||
if tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#
|
||||
var name = tok.value
|
||||
|
||||
# Check if this is a template animation parameter FIRST - before symbol table lookup
|
||||
# This allows template animation parameters to override any other symbol resolution
|
||||
if self.template_animation_params != nil && self.template_animation_params.contains(name)
|
||||
self.next()
|
||||
# This is a parameter in a template animation - return self.param reference
|
||||
# The wrapping in create_closure_value will be done at the assignment level, not here
|
||||
var param_ref = f"self.{name}"
|
||||
return self.ExpressionResult.variable_ref(param_ref, 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#, nil)
|
||||
end
|
||||
|
||||
var entry = self.symbol_table.get(name)
|
||||
|
||||
if entry == nil
|
||||
@ -2695,6 +2855,179 @@ class SimpleDSLTranspiler
|
||||
self.add("")
|
||||
end
|
||||
|
||||
# Helper method to add inherited parameters from engine_proxy class hierarchy
|
||||
# This dynamically discovers all parameters from engine_proxy and its superclasses
|
||||
def _add_inherited_params_to_template(template_params_map)
|
||||
import introspect
|
||||
|
||||
# Create a temporary engine_proxy instance to inspect its class hierarchy
|
||||
try
|
||||
var temp_engine = animation.init_strip()
|
||||
var proxy_instance = animation.engine_proxy(temp_engine)
|
||||
|
||||
# Walk up the class hierarchy to collect all PARAMS
|
||||
var current_class = classof(proxy_instance)
|
||||
while current_class != nil
|
||||
# Check if this class has PARAMS
|
||||
if introspect.contains(current_class, "PARAMS")
|
||||
var class_params = current_class.PARAMS
|
||||
# Add all parameter names from this class
|
||||
for param_name : class_params.keys()
|
||||
template_params_map[param_name] = true
|
||||
end
|
||||
end
|
||||
|
||||
# Move to parent class
|
||||
current_class = super(current_class)
|
||||
end
|
||||
except .. as e, msg
|
||||
# If we can't create the instance, fall back to a static list
|
||||
# This should include the known parameters from engine_proxy hierarchy
|
||||
var fallback_params = ["name", "priority", "duration", "loop", "opacity", "color", "is_running"]
|
||||
for param : fallback_params
|
||||
template_params_map[param] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Generate Berry class for template animation definition
|
||||
# Creates a class extending engine_proxy with parameters as instance variables
|
||||
def generate_template_animation_class(name, params, param_types)
|
||||
import animation_dsl
|
||||
import string
|
||||
|
||||
# Generate class definition
|
||||
self.add(f"# Template animation class: {name}")
|
||||
self.add(f"class {name}_animation : animation.engine_proxy")
|
||||
|
||||
# Generate PARAMS static variable with encode_constraints
|
||||
self.add(" static var PARAMS = animation.enc_params({")
|
||||
for i : 0..size(params)-1
|
||||
var param = params[i]
|
||||
var param_constraints = param_types.find(param)
|
||||
var comma = (i < size(params) - 1) ? "," : ""
|
||||
|
||||
if param_constraints != nil
|
||||
# param_constraints is now a map with type, min, max, default
|
||||
if type(param_constraints) == "instance" && classname(param_constraints) == "map"
|
||||
# Build constraint map string
|
||||
var constraint_parts = []
|
||||
if param_constraints.contains("type")
|
||||
constraint_parts.push(f'"type": "{param_constraints["type"]}"')
|
||||
end
|
||||
if param_constraints.contains("min")
|
||||
constraint_parts.push(f'"min": {param_constraints["min"]}')
|
||||
end
|
||||
if param_constraints.contains("max")
|
||||
constraint_parts.push(f'"max": {param_constraints["max"]}')
|
||||
end
|
||||
if param_constraints.contains("default")
|
||||
constraint_parts.push(f'"default": {param_constraints["default"]}')
|
||||
end
|
||||
if param_constraints.contains("nillable")
|
||||
constraint_parts.push(f'"nillable": {param_constraints["nillable"]}')
|
||||
end
|
||||
|
||||
var constraint_str = ""
|
||||
for j : 0..size(constraint_parts)-1
|
||||
constraint_str += constraint_parts[j]
|
||||
if j < size(constraint_parts) - 1
|
||||
constraint_str += ", "
|
||||
end
|
||||
end
|
||||
|
||||
self.add(f' "{param}": {{{constraint_str}}}{comma}')
|
||||
else
|
||||
# Old format - just a string type
|
||||
self.add(f' "{param}": {{"type": "{param_constraints}"}}{comma}')
|
||||
end
|
||||
else
|
||||
self.add(f' "{param}": {{}}{comma}')
|
||||
end
|
||||
end
|
||||
self.add(" })")
|
||||
self.add("")
|
||||
|
||||
# Generate setup_template method (contains all template code)
|
||||
self.add(" # Template setup method - overrides EngineProxy placeholder")
|
||||
self.add(" def setup_template()")
|
||||
self.add(" var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')")
|
||||
self.add("")
|
||||
|
||||
# Create a new transpiler that shares the same pull lexer
|
||||
# It will consume tokens from the current position until the template ends
|
||||
var template_transpiler = animation_dsl.SimpleDSLTranspiler(self.pull_lexer)
|
||||
template_transpiler.symbol_table = animation_dsl._symbol_table() # Fresh symbol table for template
|
||||
template_transpiler.strip_initialized = true # Templates assume engine exists
|
||||
template_transpiler.indent_level = 2 # Start with 2 levels of indentation (inside class and setup_template method)
|
||||
|
||||
# Set template animation parameters for special handling
|
||||
# Include both user-defined parameters AND inherited parameters from engine_proxy class hierarchy
|
||||
template_transpiler.template_animation_params = {}
|
||||
|
||||
# Add user-defined parameters
|
||||
for param : params
|
||||
template_transpiler.template_animation_params[param] = true
|
||||
end
|
||||
|
||||
# Add inherited parameters from engine_proxy class hierarchy dynamically
|
||||
self._add_inherited_params_to_template(template_transpiler.template_animation_params)
|
||||
|
||||
# Add parameters to template's symbol table with proper types
|
||||
# Mark them as special "parameter" type so they get wrapped in closures
|
||||
for param : params
|
||||
var param_constraints = param_types.find(param)
|
||||
if param_constraints != nil
|
||||
# Extract type from constraints map (or use directly if it's a string)
|
||||
var param_type = nil
|
||||
if type(param_constraints) == "instance" && classname(param_constraints) == "map"
|
||||
param_type = param_constraints.find("type")
|
||||
else
|
||||
param_type = param_constraints # Old format - just a string
|
||||
end
|
||||
|
||||
if param_type != nil
|
||||
# Create typed parameter based on type annotation
|
||||
self._add_typed_parameter_to_symbol_table(template_transpiler.symbol_table, param, param_type)
|
||||
else
|
||||
# No type specified - default to variable
|
||||
template_transpiler.symbol_table.create_variable(param)
|
||||
end
|
||||
else
|
||||
# Default to variable type for untyped parameters
|
||||
template_transpiler.symbol_table.create_variable(param)
|
||||
end
|
||||
end
|
||||
|
||||
# Transpile the template body - it will consume tokens until the closing brace
|
||||
var template_body = template_transpiler.transpile_template_animation_body()
|
||||
|
||||
if template_body != nil
|
||||
# Add the transpiled body with proper indentation (4 spaces for inside setup_template method)
|
||||
var body_lines = string.split(template_body, "\n")
|
||||
for line : body_lines
|
||||
if size(line) > 0
|
||||
self.add(f" {line}") # Add 4-space indentation for setup_template method body
|
||||
end
|
||||
end
|
||||
|
||||
# Validate parameter usage in template body (post-transpilation check)
|
||||
self._validate_template_parameter_usage(name, params, template_body)
|
||||
else
|
||||
# Error in template body transpilation
|
||||
for error : template_transpiler.errors
|
||||
self.error(f"Template animation '{name}' body error: {error}")
|
||||
end
|
||||
end
|
||||
|
||||
# Expect the closing brace (template_transpiler should have left us at this position)
|
||||
self.expect_right_brace()
|
||||
|
||||
self.add(" end")
|
||||
self.add("end")
|
||||
self.add("")
|
||||
end
|
||||
|
||||
# Process named arguments for animation declarations with parameter validation
|
||||
#
|
||||
# @param var_name: string - Variable name to assign parameters to
|
||||
@ -2879,7 +3212,7 @@ class SimpleDSLTranspiler
|
||||
# Template parameter validation methods
|
||||
|
||||
# Validate template parameter name
|
||||
def _validate_template_parameter_name(param_name, param_names_seen)
|
||||
def _validate_template_parameter_name(param_name, param_names_seen, is_template_animation)
|
||||
import animation_dsl
|
||||
# Check for duplicate parameter names
|
||||
if param_names_seen.contains(param_name)
|
||||
@ -2907,14 +3240,28 @@ class SimpleDSLTranspiler
|
||||
return false
|
||||
end
|
||||
|
||||
# For template animations, check if parameter masks an existing parameter from EngineProxy or Animation
|
||||
if is_template_animation
|
||||
var base_class_params = [
|
||||
"name", "is_running", "priority", "duration", "loop", "opacity", "color"
|
||||
]
|
||||
|
||||
for base_param : base_class_params
|
||||
if param_name == base_param
|
||||
self.warning(f"Template animation parameter '{param_name}' masks existing parameter from EngineProxy base class. This may cause unexpected behavior. Consider using a different name like 'custom_{param_name}' or '{param_name}_value'.")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Validate template parameter type annotation
|
||||
def _validate_template_parameter_type(param_type)
|
||||
var valid_types = [
|
||||
"color", "palette", "animation", "number", "string", "boolean",
|
||||
"time", "percentage", "variable", "value_provider"
|
||||
"int", "bool", "string", "bytes", "function", "animation",
|
||||
"value_provider", "number", "color", "palette", "time", "percentage", "any"
|
||||
]
|
||||
|
||||
for valid_type : valid_types
|
||||
@ -2927,6 +3274,138 @@ class SimpleDSLTranspiler
|
||||
return false
|
||||
end
|
||||
|
||||
# Register template animation as an animation constructor
|
||||
# This allows it to be used like: animation x = template_name(param1=value1, ...)
|
||||
def _register_template_animation_constructor(name, params, param_types)
|
||||
import animation_dsl
|
||||
|
||||
# Create a mock instance that has _has_param method for validation
|
||||
var mock_instance = {
|
||||
"_params": {},
|
||||
"_has_param": def (param_name)
|
||||
# Check if this parameter exists in the template's parameter list
|
||||
for p : params
|
||||
if p == param_name
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
# Add all parameters to the mock instance's _params
|
||||
for param : params
|
||||
mock_instance["_params"][param] = true
|
||||
end
|
||||
|
||||
# Get the existing template entry and update it to be an animation constructor
|
||||
var existing_entry = self.symbol_table.entries.find(name)
|
||||
if existing_entry != nil
|
||||
# Update the existing entry to be an animation constructor type
|
||||
existing_entry.type = 8 # TYPE_ANIMATION_CONSTRUCTOR
|
||||
existing_entry.instance = mock_instance
|
||||
existing_entry.takes_args = true
|
||||
existing_entry.arg_type = "named"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse parameter constraints (type, min, max, default)
|
||||
# Returns a map with constraint keys and values, or nil if no constraints
|
||||
def _parse_parameter_constraints()
|
||||
var constraints = {}
|
||||
|
||||
# Parse all constraint keywords until we hit a newline or end of constraints
|
||||
while !self.at_end()
|
||||
var tok = self.current()
|
||||
|
||||
# Stop if we hit a newline or closing brace
|
||||
if tok == nil || tok.type == 35 #-animation_dsl.Token.NEWLINE-# || tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-#
|
||||
break
|
||||
end
|
||||
|
||||
# Check for constraint keywords (can be either KEYWORD or IDENTIFIER tokens)
|
||||
if tok.type == 0 #-animation_dsl.Token.KEYWORD-# || tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#
|
||||
if tok.value == "type"
|
||||
self.next() # skip 'type'
|
||||
var param_type = self.expect_identifier()
|
||||
|
||||
# Validate type annotation
|
||||
if !self._validate_template_parameter_type(param_type)
|
||||
return nil
|
||||
end
|
||||
|
||||
constraints["type"] = param_type
|
||||
|
||||
elif tok.value == "min"
|
||||
self.next() # skip 'min'
|
||||
# Use process_value to handle all value types (numbers, time, colors, etc.)
|
||||
var min_result = self.process_value(self.CONTEXT_GENERIC)
|
||||
if min_result != nil && min_result.expr != nil
|
||||
# Try to evaluate the expression to get a concrete value
|
||||
# For simple literals, the expr will be the value itself
|
||||
constraints["min"] = min_result.expr
|
||||
else
|
||||
self.error("Expected value after 'min'")
|
||||
return nil
|
||||
end
|
||||
|
||||
elif tok.value == "max"
|
||||
self.next() # skip 'max'
|
||||
# Use process_value to handle all value types (numbers, time, colors, etc.)
|
||||
var max_result = self.process_value(self.CONTEXT_GENERIC)
|
||||
if max_result != nil && max_result.expr != nil
|
||||
# Try to evaluate the expression to get a concrete value
|
||||
# For simple literals, the expr will be the value itself
|
||||
constraints["max"] = max_result.expr
|
||||
else
|
||||
self.error("Expected value after 'max'")
|
||||
return nil
|
||||
end
|
||||
|
||||
elif tok.value == "default"
|
||||
self.next() # skip 'default'
|
||||
# Use process_value to handle all value types (numbers, time, colors, etc.)
|
||||
var default_result = self.process_value(self.CONTEXT_GENERIC)
|
||||
if default_result != nil && default_result.expr != nil
|
||||
# Store the expression as the default value
|
||||
constraints["default"] = default_result.expr
|
||||
else
|
||||
self.error("Expected value after 'default'")
|
||||
return nil
|
||||
end
|
||||
|
||||
elif tok.value == "nillable"
|
||||
self.next() # skip 'nillable'
|
||||
var nillable_tok = self.current()
|
||||
if nillable_tok != nil && nillable_tok.type == 0 #-animation_dsl.Token.KEYWORD-#
|
||||
if nillable_tok.value == "true"
|
||||
self.next()
|
||||
constraints["nillable"] = true
|
||||
elif nillable_tok.value == "false"
|
||||
self.next()
|
||||
constraints["nillable"] = false
|
||||
else
|
||||
self.error("Expected 'true' or 'false' after 'nillable'")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
self.error("Expected 'true' or 'false' after 'nillable'")
|
||||
return nil
|
||||
end
|
||||
|
||||
else
|
||||
# Unknown keyword - stop parsing constraints
|
||||
break
|
||||
end
|
||||
else
|
||||
# Not a keyword or identifier - stop parsing constraints
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return size(constraints) > 0 ? constraints : nil
|
||||
end
|
||||
|
||||
# Add typed parameter to symbol table based on type annotation
|
||||
def _add_typed_parameter_to_symbol_table(symbol_table, param_name, param_type)
|
||||
if param_type == "color"
|
||||
@ -2938,7 +3417,7 @@ class SimpleDSLTranspiler
|
||||
elif param_type == "value_provider"
|
||||
symbol_table.create_value_provider(param_name, nil)
|
||||
else
|
||||
# Default to variable for number, string, boolean, time, percentage, variable
|
||||
# Default to variable for number, string, bool, time, percentage, function
|
||||
symbol_table.create_variable(param_name)
|
||||
end
|
||||
end
|
||||
@ -2949,9 +3428,11 @@ class SimpleDSLTranspiler
|
||||
|
||||
# Check if each parameter is actually used in the template body
|
||||
for param : params
|
||||
var param_ref = f"{param}_" # Parameters are referenced with underscore suffix
|
||||
# Check for both regular template usage (param_) and template animation usage (self.param)
|
||||
var param_ref_regular = f"{param}_"
|
||||
var param_ref_animation = f"self.{param}"
|
||||
|
||||
if string.find(template_body, param_ref) == -1
|
||||
if string.find(template_body, param_ref_regular) == -1 && string.find(template_body, param_ref_animation) == -1
|
||||
# Parameter not found in body - this is a warning, not an error
|
||||
self.warning(f"Template '{template_name}' parameter '{param}' is declared but never used in the template body.")
|
||||
end
|
||||
|
||||
@ -15,7 +15,7 @@ import "./core/param_encoder" as encode_constraints
|
||||
class BreatheColorProvider : animation.oscillator_value
|
||||
# Additional parameter definitions for color-specific functionality
|
||||
# The oscillator parameters (min_value, max_value, duration, form, etc.) are inherited
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"base_color": {"default": 0xFFFFFFFF}, # The base color to modulate (32-bit ARGB value)
|
||||
"min_brightness": {"min": 0, "max": 255, "default": 0}, # Minimum brightness level (0-255)
|
||||
"max_brightness": {"min": 0, "max": 255, "default": 255}, # Maximum brightness level (0-255)
|
||||
|
||||
@ -22,7 +22,7 @@ class ClosureValueProvider : animation.value_provider
|
||||
var _closure # We keep the closure as instance variable for faster dereferencing, in addition to PARAMS
|
||||
|
||||
# Static parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"closure": {"type": "function", "default": nil}
|
||||
})
|
||||
|
||||
|
||||
@ -16,11 +16,10 @@ import "./core/param_encoder" as encode_constraints
|
||||
#@ solidify:ColorCycleColorProvider,weak
|
||||
class ColorCycleColorProvider : animation.color_provider
|
||||
# Non-parameter instance variables only
|
||||
var current_color # Current interpolated color (calculated during update)
|
||||
var current_index # Current color index for next functionality
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"palette": {"type": "bytes", "default":
|
||||
bytes( # Palette bytes in AARRGGBB format
|
||||
"FF0000FF" # Blue
|
||||
@ -40,49 +39,68 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
super(self).init(engine) # Initialize parameter system
|
||||
|
||||
# Initialize non-parameter instance variables
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
self.current_color = self._get_color_at_index(0) # Start with first color in palette
|
||||
self.current_index = 0 # Start at first color
|
||||
var palette_bytes = self.palette
|
||||
self.current_index = 0 # Start at first color
|
||||
|
||||
# Initialize palette_size parameter
|
||||
self.values["palette_size"] = self._get_palette_size()
|
||||
end
|
||||
|
||||
# Get palette bytes from parameter with default fallback
|
||||
def _get_palette_bytes()
|
||||
var palette_bytes = self.palette
|
||||
if palette_bytes == nil
|
||||
# Get default from PARAMS using encoded constraints
|
||||
var encoded_constraints = self._get_param_def("palette")
|
||||
if encoded_constraints != nil && self.constraint_mask(encoded_constraints, "default")
|
||||
palette_bytes = self.constraint_find(encoded_constraints, "default", nil)
|
||||
end
|
||||
end
|
||||
return palette_bytes
|
||||
end
|
||||
|
||||
# Get color at a specific index from bytes palette
|
||||
# We force alpha channel to 0xFF to force opaque colors
|
||||
def _get_color_at_index(idx)
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
var palette_bytes = self.palette
|
||||
var palette_size = size(palette_bytes) / 4 # Each color is 4 bytes (AARRGGBB)
|
||||
|
||||
if palette_size == 0 || idx < 0 || idx >= palette_size
|
||||
return 0xFFFFFFFF # Default to white
|
||||
if (palette_size == 0) || (idx >= palette_size) || (idx < 0)
|
||||
return 0x00000000 # Default to transparent
|
||||
end
|
||||
|
||||
# Read 4 bytes in big-endian format (AARRGGBB)
|
||||
var color = palette_bytes.get(idx * 4, -4) # Big endian
|
||||
color = color | 0xFF000000
|
||||
color = color | 0xFF000000 # force full opacity
|
||||
return color
|
||||
end
|
||||
|
||||
# Get the number of colors in the palette
|
||||
def _get_palette_size()
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
return size(palette_bytes) / 4 # Each color is 4 bytes
|
||||
return size( self.palette) / 4 # Each color is 4 bytes
|
||||
end
|
||||
|
||||
# Virtual member access - implements the virtual "palette_size" attribute
|
||||
#
|
||||
# @param name: string - Parameter name being accessed
|
||||
# @return any - Resolved parameter value (ValueProvider resolved to actual value)
|
||||
def member(name)
|
||||
if name == "palette_size"
|
||||
return self._get_palette_size()
|
||||
else
|
||||
return super(self).member(name)
|
||||
end
|
||||
end
|
||||
|
||||
# Adjust index according to palette_size
|
||||
#
|
||||
# @param palette_size: int - Size of palette in colors, passed as parameter to avoid recalculating it
|
||||
def _adjust_index()
|
||||
var palette_size = self._get_palette_size()
|
||||
if palette_size > 0
|
||||
# Apply modulo palette size
|
||||
var index = self.current_index % palette_size
|
||||
# It is still possible to be negative
|
||||
if index < 0
|
||||
index += palette_size
|
||||
end
|
||||
# If index changed, invalidate color
|
||||
if self.current_index != index
|
||||
self.current_index = index
|
||||
end
|
||||
|
||||
else
|
||||
self.current_index = 0 # default value when empty palette
|
||||
end
|
||||
end
|
||||
|
||||
# Handle parameter changes
|
||||
#
|
||||
# @param name: string - Name of the parameter that changed
|
||||
@ -93,31 +111,14 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
# palette_size is read-only - restore the actual value and raise an exception
|
||||
self.values["palette_size"] = self._get_palette_size()
|
||||
raise "value_error", "Parameter 'palette_size' is read-only"
|
||||
elif name == "palette"
|
||||
# When palette changes, update current_color if current_index is valid
|
||||
var palette_size = self._get_palette_size()
|
||||
if palette_size > 0
|
||||
# Clamp current_index to valid range
|
||||
if self.current_index >= palette_size
|
||||
self.current_index = 0
|
||||
end
|
||||
self.current_color = self._get_color_at_index(self.current_index)
|
||||
end
|
||||
# Update palette_size parameter
|
||||
self.values["palette_size"] = palette_size
|
||||
|
||||
elif name == "next" && value != 0
|
||||
# Add to color index
|
||||
var palette_size = self._get_palette_size()
|
||||
if palette_size > 0
|
||||
var current_index = (self.current_index + value) % palette_size
|
||||
if current_index < 0
|
||||
current_index += palette_size
|
||||
end
|
||||
self.current_index = current_index
|
||||
self.current_color = self._get_color_at_index(self.current_index)
|
||||
end
|
||||
self.current_index += value
|
||||
self._adjust_index()
|
||||
|
||||
# Reset the next parameter back to 0
|
||||
self.set_param("next", 0)
|
||||
self.values["next"] = 0
|
||||
end
|
||||
end
|
||||
|
||||
@ -132,21 +133,13 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
|
||||
# Get the number of colors in the palette
|
||||
var palette_size = self._get_palette_size()
|
||||
if palette_size == 0
|
||||
return 0xFFFFFFFF # Default to white if no colors
|
||||
end
|
||||
|
||||
if palette_size == 1
|
||||
# If only one color, just return it
|
||||
self.current_color = self._get_color_at_index(0)
|
||||
return self.current_color
|
||||
end
|
||||
|
||||
# Check if cycle_period is 0 (manual-only mode)
|
||||
if cycle_period == 0
|
||||
# Manual-only mode: colors only change when 'next' parameter is set to 1
|
||||
# Return the current color without any time-based changes
|
||||
return self.current_color
|
||||
|
||||
if (palette_size <= 1) || (cycle_period == 0) # no cycling stop here
|
||||
var idx = self.current_index
|
||||
if (idx >= palette_size) idx = palette_size - 1 end
|
||||
if (idx < 0) idx = 0 end
|
||||
self.current_index = idx
|
||||
return self._get_color_at_index(self.current_index)
|
||||
end
|
||||
|
||||
# Auto-cycle mode: calculate which color to show based on time (brutal switching using integer math)
|
||||
@ -160,9 +153,7 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
|
||||
# Update current state and return the color
|
||||
self.current_index = color_index
|
||||
self.current_color = self._get_color_at_index(color_index)
|
||||
|
||||
return self.current_color
|
||||
return self._get_color_at_index(color_index)
|
||||
end
|
||||
|
||||
# Get a color based on a value (maps value to position in cycle)
|
||||
@ -175,7 +166,7 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
# Get the number of colors in the palette
|
||||
var palette_size = self._get_palette_size()
|
||||
if palette_size == 0
|
||||
return 0xFFFFFFFF # Default to white if no colors
|
||||
return 0x00000000 # Default to transparent if no colors
|
||||
end
|
||||
|
||||
if palette_size == 1
|
||||
|
||||
@ -15,7 +15,7 @@ class CompositeColorProvider : animation.color_provider
|
||||
var providers # List of color providers
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"blend_mode": {"enum": [0, 1, 2], "default": 0} # 0=overlay, 1=add, 2=multiply
|
||||
})
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ import "./core/param_encoder" as encode_constraints
|
||||
#@ solidify:IterationNumberProvider,weak
|
||||
class IterationNumberProvider : animation.value_provider
|
||||
# Static parameter definitions (no parameters needed)
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
|
||||
})
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ class OscillatorValueProvider : animation.value_provider
|
||||
static var form_names = ["", "SAWTOOTH", "TRIANGLE", "SQUARE", "COSINE", "SINE", "EASE_IN", "EASE_OUT", "ELASTIC", "BOUNCE"]
|
||||
|
||||
# Parameter definitions for the oscillator
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"min_value": {"default": 0},
|
||||
"max_value": {"default": 100},
|
||||
"duration": {"min": 1, "default": 1000},
|
||||
|
||||
@ -19,7 +19,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
var light_state # light_state instance for proper color calculations
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"palette": {"type": "bytes", "default": nil}, # Palette bytes or predefined palette constant
|
||||
"cycle_period": {"min": 0, "default": 5000}, # 5 seconds default, 0 = value-based only
|
||||
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR},
|
||||
|
||||
@ -12,7 +12,7 @@ import "./core/param_encoder" as encode_constraints
|
||||
#@ solidify:StaticColorProvider,weak
|
||||
class StaticColorProvider : animation.color_provider
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"color": {"default": 0xFFFFFFFF} # Default to white
|
||||
})
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ import "./core/param_encoder" as encode_constraints
|
||||
#@ solidify:StaticValueProvider,weak
|
||||
class StaticValueProvider : animation.value_provider
|
||||
# Parameter definitions
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"value": {"default": nil, "type": "any"}
|
||||
})
|
||||
|
||||
|
||||
@ -19,12 +19,13 @@ class StripLengthProvider : animation.value_provider
|
||||
# @param time_ms: int - Current time in milliseconds (ignored)
|
||||
# @return int - The strip length in pixels
|
||||
def produce_value(name, time_ms)
|
||||
return self.engine ? self.engine.width : 0
|
||||
return (self.engine != nil) ? self.engine.get_strip_length() : 0
|
||||
end
|
||||
|
||||
# String representation of the provider
|
||||
def tostring()
|
||||
return f"StripLengthProvider(length={self.engine ? self.engine.width :: 'unknown'})"
|
||||
var strip_width = (self.engine != nil) ? self.engine.get_strip_length() : 'unknown'
|
||||
return f"StripLengthProvider(length={strip_width})"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ import "./core/param_encoder" as encode_constraints
|
||||
#@ solidify:ValueProvider,weak
|
||||
class ValueProvider : animation.parameterized_object
|
||||
# Static parameter definitions - can be overridden by subclasses
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -70,9 +70,9 @@ assert_test(!engine.add(anim1), "Should not add duplicate animation")
|
||||
assert_equals(engine.size(), 3, "Size should remain 3 after duplicate attempt")
|
||||
|
||||
# Test animation removal
|
||||
assert_test(engine.remove_animation(anim2), "Should remove existing animation")
|
||||
assert_test(engine.remove(anim2), "Should remove existing animation")
|
||||
assert_equals(engine.size(), 2, "Size should be 2 after removal")
|
||||
assert_test(!engine.remove_animation(anim2), "Should not remove non-existent animation")
|
||||
assert_test(!engine.remove(anim2), "Should not remove non-existent animation")
|
||||
|
||||
# Test 3: Engine Lifecycle
|
||||
print("\n--- Test 3: Engine Lifecycle ---")
|
||||
@ -106,13 +106,13 @@ assert_test(true, "Engine should process tick without error")
|
||||
|
||||
# Test 5: Sequence Manager Integration
|
||||
print("\n--- Test 5: Sequence Manager Integration ---")
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
assert_not_nil(seq_manager, "Sequence manager should be created")
|
||||
|
||||
engine.add(seq_manager)
|
||||
assert_test(true, "Should add sequence manager without error")
|
||||
|
||||
engine.remove_sequence_manager(seq_manager)
|
||||
engine.remove(seq_manager)
|
||||
assert_test(true, "Should remove sequence manager without error")
|
||||
|
||||
# Test 6: Clear Functionality
|
||||
|
||||
@ -318,14 +318,16 @@ 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
|
||||
# Verify opacity frame buffers are created when needed
|
||||
# Note: Opacity frames are created lazily on first render with animation opacity
|
||||
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})")
|
||||
# With composition architecture, opacity frames are created on-demand
|
||||
assert_test(opacity_frames_created >= 0, f"Opacity frame buffers created as needed (found {opacity_frames_created})")
|
||||
|
||||
opacity_engine.stop()
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ assert(default_anim.duration == 0, "Default duration should be 0 (infinite)")
|
||||
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")
|
||||
assert(default_anim.color == 0x00000000, "Default color should be transparent")
|
||||
|
||||
# Test start method
|
||||
engine.time_ms = 1000
|
||||
|
||||
@ -16,7 +16,7 @@ def test_atomic_closure_batch_execution()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create two test animations
|
||||
var red_provider = animation.static_color(engine)
|
||||
@ -86,7 +86,7 @@ def test_multiple_consecutive_closures()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test animations
|
||||
var green_provider = animation.static_color(engine)
|
||||
@ -155,7 +155,7 @@ def test_closure_batch_at_sequence_start()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test animation
|
||||
var purple_provider = animation.static_color(engine)
|
||||
@ -211,7 +211,7 @@ def test_repeat_sequence_closure_batching()
|
||||
var iteration_closure = def (engine) iteration_count += 1 end
|
||||
|
||||
# Create repeating sequence with closure
|
||||
var seq_manager = animation.SequenceManager(engine, 3) # Repeat 3 times
|
||||
var seq_manager = animation.sequence_manager(engine, 3) # Repeat 3 times
|
||||
seq_manager.push_closure_step(iteration_closure)
|
||||
.push_play_step(cyan_anim, 30) # Very short for fast testing
|
||||
|
||||
@ -286,7 +286,7 @@ def test_black_frame_fix_integration()
|
||||
# play shutter_animation for 200ms
|
||||
# col1.next = 1
|
||||
# }
|
||||
var seq_manager = animation.SequenceManager(engine, 5)
|
||||
var seq_manager = animation.sequence_manager(engine, 5)
|
||||
seq_manager.push_play_step(shutter_anim, 200)
|
||||
.push_closure_step(advance_color)
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import "./core/param_encoder" as encode_constraints
|
||||
|
||||
# Test class that uses bytes parameter
|
||||
class BytesTestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"data": {"type": "bytes", "default": nil, "nillable": true},
|
||||
"required_data": {"type": "bytes"},
|
||||
"name": {"type": "string", "default": "test"}
|
||||
@ -25,6 +25,10 @@ class MockEngine
|
||||
def init()
|
||||
self.time_ms = 1000
|
||||
end
|
||||
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
def test_bytes_type_validation()
|
||||
|
||||
@ -14,6 +14,10 @@ def test_closure_value_provider()
|
||||
def init()
|
||||
self.time_ms = 1000
|
||||
end
|
||||
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
var engine = MockEngine()
|
||||
|
||||
|
||||
@ -80,14 +80,13 @@ def test_color_cycle_bytes_format()
|
||||
# Test 7: Test manual mode
|
||||
provider.cycle_period = 0 # Manual mode
|
||||
provider.current_index = 1
|
||||
provider.current_color = custom_color1
|
||||
|
||||
var manual_color = provider.produce_value("color", 5000)
|
||||
assert(manual_color == custom_color1, f"Manual mode should return current color")
|
||||
|
||||
# Test 8: Test next functionality
|
||||
provider.next = 1 # Should trigger move to next color
|
||||
var next_color = provider.current_color
|
||||
var next_color = provider.produce_value("color", 5000)
|
||||
assert(next_color == custom_color2, f"Next should move to third color")
|
||||
assert(provider.current_index == 2, f"Current index should be 2")
|
||||
|
||||
@ -101,10 +100,10 @@ def test_color_cycle_bytes_format()
|
||||
|
||||
# Test 10: Test edge cases
|
||||
var invalid_color = provider._get_color_at_index(-1) # Invalid index
|
||||
assert(invalid_color == 0xFFFFFFFF, f"Invalid index should return white")
|
||||
assert(invalid_color == 0x00000000, f"Invalid index should return transparent")
|
||||
|
||||
var out_of_bounds_color = provider._get_color_at_index(100) # Out of bounds
|
||||
assert(out_of_bounds_color == 0xFFFFFFFF, f"Out of bounds index should return white")
|
||||
assert(out_of_bounds_color == 0x00000000, f"Out of bounds index should return transparent")
|
||||
|
||||
# Test 11: Test empty palette handling
|
||||
var empty_palette = bytes()
|
||||
@ -113,7 +112,7 @@ def test_color_cycle_bytes_format()
|
||||
assert(empty_size == 0, f"Empty palette should have 0 colors")
|
||||
|
||||
var empty_color = provider.produce_value("color", 1000)
|
||||
assert(empty_color == 0xFFFFFFFF, f"Empty palette should return white")
|
||||
assert(empty_color == 0x00000000, f"Empty palette should return transparent")
|
||||
|
||||
print("✓ All ColorCycleColorProvider bytes format tests passed!")
|
||||
end
|
||||
@ -127,7 +126,7 @@ def test_bytes_parameter_validation()
|
||||
# Test 1: Valid bytes palette should be accepted
|
||||
var valid_palette = bytes("FF0000FFFF00FF00FFFF0000")
|
||||
provider.palette = valid_palette
|
||||
assert(provider._get_palette_size() == 3, "Valid bytes palette should be accepted")
|
||||
assert(provider.palette_size == 3, "Valid bytes palette should be accepted")
|
||||
|
||||
# Test 2: Invalid types should be rejected
|
||||
var invalid_types = ["string", 123, 3.14, true, [], {}]
|
||||
@ -144,7 +143,7 @@ def test_bytes_parameter_validation()
|
||||
|
||||
# Test 3: Nil should be accepted (uses default)
|
||||
provider.palette = nil
|
||||
assert(provider._get_palette_size() == 3, "Nil should use default palette")
|
||||
assert(provider.palette_size == 3, "Nil should use default palette")
|
||||
|
||||
print("✓ All bytes parameter validation tests passed!")
|
||||
end
|
||||
|
||||
@ -50,7 +50,7 @@ assert_not_nil(comet, "Comet animation should be created")
|
||||
assert_equals(comet.engine, engine, "Animation should have correct engine reference")
|
||||
|
||||
# Test default values
|
||||
assert_equals(comet.color, 0xFFFFFFFF, "Default color should be white")
|
||||
assert_equals(comet.color, 0x00000000, "Default color should be transparent")
|
||||
assert_equals(comet.tail_length, 5, "Default tail length should be 5")
|
||||
assert_equals(comet.speed, 2560, "Default speed should be 2560")
|
||||
assert_equals(comet.direction, 1, "Default direction should be 1 (forward)")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Constraint Encoding Test Suite
|
||||
#
|
||||
# Comprehensive tests for encode_constraints() and ParameterizedObject static methods:
|
||||
# Comprehensive tests for animation.enc_params() and ParameterizedObject static methods:
|
||||
# - constraint_mask()
|
||||
# - constraint_find()
|
||||
#
|
||||
@ -64,7 +64,7 @@ print("\n--- Test Group 1: Basic Integer Constraints ---")
|
||||
|
||||
# Test 1.1: Simple min/max/default (int8 range)
|
||||
var params_1_1 = {"min": 0, "max": 255, "default": 128}
|
||||
var encoded_1_1 = encode_constraints({"test": params_1_1})["test"]
|
||||
var encoded_1_1 = animation.enc_params({"test": params_1_1})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_1, "min"), 0x01, "1.1a: has min")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_1, "max"), 0x02, "1.1b: has max")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_1, "default"), 0x04, "1.1c: has default")
|
||||
@ -74,7 +74,7 @@ assert_equal(animation.parameterized_object.constraint_find(encoded_1_1, "defaul
|
||||
|
||||
# Test 1.2: Only default (no min/max)
|
||||
var params_1_2 = {"default": 0xFFFFFFFF}
|
||||
var encoded_1_2 = encode_constraints({"test": params_1_2})["test"]
|
||||
var encoded_1_2 = animation.enc_params({"test": params_1_2})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_2, "min"), 0x00, "1.2a: no min")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_2, "max"), 0x00, "1.2b: no max")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_2, "default"), 0x04, "1.2c: has default")
|
||||
@ -82,7 +82,7 @@ assert_equal(animation.parameterized_object.constraint_find(encoded_1_2, "defaul
|
||||
|
||||
# Test 1.3: Min only
|
||||
var params_1_3 = {"min": 1, "default": 1000}
|
||||
var encoded_1_3 = encode_constraints({"test": params_1_3})["test"]
|
||||
var encoded_1_3 = animation.enc_params({"test": params_1_3})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_3, "min"), 0x01, "1.3a: has min")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_1_3, "max"), 0x00, "1.3b: no max")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_1_3, "min", nil), 1, "1.3c: min value")
|
||||
@ -90,14 +90,14 @@ assert_equal(animation.parameterized_object.constraint_find(encoded_1_3, "defaul
|
||||
|
||||
# Test 1.4: Negative values
|
||||
var params_1_4 = {"min": -128, "max": 127, "default": 0}
|
||||
var encoded_1_4 = encode_constraints({"test": params_1_4})["test"]
|
||||
var encoded_1_4 = animation.enc_params({"test": params_1_4})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_1_4, "min", nil), -128, "1.4a: negative min")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_1_4, "max", nil), 127, "1.4b: positive max")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_1_4, "default", nil), 0, "1.4c: zero default")
|
||||
|
||||
# Test 1.5: Large int32 values
|
||||
var params_1_5 = {"min": 0, "max": 25600, "default": 2560}
|
||||
var encoded_1_5 = encode_constraints({"test": params_1_5})["test"]
|
||||
var encoded_1_5 = animation.enc_params({"test": params_1_5})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_1_5, "max", nil), 25600, "1.5a: large max")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_1_5, "default", nil), 2560, "1.5b: large default")
|
||||
|
||||
@ -108,7 +108,7 @@ print("\n--- Test Group 2: Enum Constraints ---")
|
||||
|
||||
# Test 2.1: Simple enum with positive values
|
||||
var params_2_1 = {"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9], "default": 1}
|
||||
var encoded_2_1 = encode_constraints({"test": params_2_1})["test"]
|
||||
var encoded_2_1 = animation.enc_params({"test": params_2_1})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_2_1, "enum"), 0x10, "2.1a: has enum")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_2_1, "default", nil), 1, "2.1b: default value")
|
||||
var enum_2_1 = animation.parameterized_object.constraint_find(encoded_2_1, "enum", nil)
|
||||
@ -116,13 +116,13 @@ assert_array_equal(enum_2_1, [1, 2, 3, 4, 5, 6, 7, 8, 9], "2.1c: enum values")
|
||||
|
||||
# Test 2.2: Enum with negative values
|
||||
var params_2_2 = {"enum": [-1, 1], "default": 1}
|
||||
var encoded_2_2 = encode_constraints({"test": params_2_2})["test"]
|
||||
var encoded_2_2 = animation.enc_params({"test": params_2_2})["test"]
|
||||
var enum_2_2 = animation.parameterized_object.constraint_find(encoded_2_2, "enum", nil)
|
||||
assert_array_equal(enum_2_2, [-1, 1], "2.2a: enum with negative values")
|
||||
|
||||
# Test 2.3: Enum with min/max/default
|
||||
var params_2_3 = {"min": 0, "max": 3, "enum": [0, 1, 2, 3], "default": 0}
|
||||
var encoded_2_3 = encode_constraints({"test": params_2_3})["test"]
|
||||
var encoded_2_3 = animation.enc_params({"test": params_2_3})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_2_3, "min"), 0x01, "2.3a: has min")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_2_3, "max"), 0x02, "2.3b: has max")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_2_3, "enum"), 0x10, "2.3c: has enum")
|
||||
@ -136,46 +136,46 @@ print("\n--- Test Group 3: Type Annotations ---")
|
||||
|
||||
# Test 3.1: Bool type
|
||||
var params_3_1 = {"type": "bool", "default": false}
|
||||
var encoded_3_1 = encode_constraints({"test": params_3_1})["test"]
|
||||
var encoded_3_1 = animation.enc_params({"test": params_3_1})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_1, "type"), 0x08, "3.1a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_1, "type", nil), "bool", "3.1b: type is bool")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_1, "default", nil), false, "3.1c: default is false")
|
||||
|
||||
# Test 3.2: String type
|
||||
var params_3_2 = {"type": "string", "default": "animation"}
|
||||
var encoded_3_2 = encode_constraints({"test": params_3_2})["test"]
|
||||
var encoded_3_2 = animation.enc_params({"test": params_3_2})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_2, "type"), 0x08, "3.2a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_2, "type", nil), "string", "3.2b: type is string")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_2, "default", nil), "animation", "3.2c: default string")
|
||||
|
||||
# Test 3.3: Int type (explicit)
|
||||
var params_3_3 = {"type": "int", "default": 3}
|
||||
var encoded_3_3 = encode_constraints({"test": params_3_3})["test"]
|
||||
var encoded_3_3 = animation.enc_params({"test": params_3_3})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_3, "type"), 0x08, "3.3a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_3, "type", nil), "int", "3.3b: type is int")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_3, "default", nil), 3, "3.3c: default int")
|
||||
|
||||
# Test 3.4: Any type
|
||||
var params_3_4 = {"type": "any", "default": 255}
|
||||
var encoded_3_4 = encode_constraints({"test": params_3_4})["test"]
|
||||
var encoded_3_4 = animation.enc_params({"test": params_3_4})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_4, "type"), 0x08, "3.4a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_4, "type", nil), "any", "3.4b: type is any")
|
||||
|
||||
# Test 3.5: Instance type
|
||||
var params_3_5 = {"type": "instance", "default": nil}
|
||||
var encoded_3_5 = encode_constraints({"test": params_3_5})["test"]
|
||||
var encoded_3_5 = animation.enc_params({"test": params_3_5})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_5, "type"), 0x08, "3.5a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_5, "type", nil), "instance", "3.5b: type is instance")
|
||||
|
||||
# Test 3.6: Function type
|
||||
var params_3_6 = {"type": "function"}
|
||||
var encoded_3_6 = encode_constraints({"test": params_3_6})["test"]
|
||||
var encoded_3_6 = animation.enc_params({"test": params_3_6})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_6, "type"), 0x08, "3.6a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_6, "type", nil), "function", "3.6b: type is function")
|
||||
|
||||
# Test 3.7: Bytes type
|
||||
var params_3_7 = {"type": "bytes", "default": bytes("FF0000FF")}
|
||||
var encoded_3_7 = encode_constraints({"test": params_3_7})["test"]
|
||||
var encoded_3_7 = animation.enc_params({"test": params_3_7})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_3_7, "type"), 0x08, "3.7a: has type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_3_7, "type", nil), "bytes", "3.7b: type is bytes")
|
||||
# Note: bytes comparison would need special handling
|
||||
@ -187,7 +187,7 @@ print("\n--- Test Group 4: Nillable Constraints ---")
|
||||
|
||||
# Test 4.1: Nillable with nil default
|
||||
var params_4_1 = {"default": nil, "nillable": true}
|
||||
var encoded_4_1 = encode_constraints({"test": params_4_1})["test"]
|
||||
var encoded_4_1 = animation.enc_params({"test": params_4_1})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_4_1, "nillable"), 0x20, "4.1a: has nillable")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_4_1, "default"), 0x04, "4.1b: has default")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_4_1, "nillable", false), true, "4.1c: nillable is true")
|
||||
@ -195,13 +195,13 @@ assert_equal(animation.parameterized_object.constraint_find(encoded_4_1, "defaul
|
||||
|
||||
# Test 4.2: Nillable without explicit default
|
||||
var params_4_2 = {"nillable": true}
|
||||
var encoded_4_2 = encode_constraints({"test": params_4_2})["test"]
|
||||
var encoded_4_2 = animation.enc_params({"test": params_4_2})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_4_2, "nillable"), 0x20, "4.2a: has nillable")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_4_2, "nillable", false), true, "4.2b: nillable is true")
|
||||
|
||||
# Test 4.3: Non-nillable (default behavior)
|
||||
var params_4_3 = {"default": 0}
|
||||
var encoded_4_3 = encode_constraints({"test": params_4_3})["test"]
|
||||
var encoded_4_3 = animation.enc_params({"test": params_4_3})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_4_3, "nillable"), 0x00, "4.3a: no nillable flag")
|
||||
|
||||
# ============================================================================
|
||||
@ -217,7 +217,7 @@ var beacon_params = {
|
||||
"beacon_size": {"min": 0, "default": 1},
|
||||
"slew_size": {"min": 0, "default": 0}
|
||||
}
|
||||
var beacon_encoded = encode_constraints(beacon_params)
|
||||
var beacon_encoded = animation.enc_params(beacon_params)
|
||||
assert_equal(animation.parameterized_object.constraint_find(beacon_encoded["color"], "default", nil), 0xFFFFFFFF, "5.1a: beacon color")
|
||||
assert_equal(animation.parameterized_object.constraint_find(beacon_encoded["beacon_size"], "min", nil), 0, "5.1b: beacon_size min")
|
||||
|
||||
@ -229,7 +229,7 @@ var comet_params = {
|
||||
"wrap_around": {"min": 0, "max": 1, "default": 1},
|
||||
"fade_factor": {"min": 0, "max": 255, "default": 179}
|
||||
}
|
||||
var comet_encoded = encode_constraints(comet_params)
|
||||
var comet_encoded = animation.enc_params(comet_params)
|
||||
assert_equal(animation.parameterized_object.constraint_find(comet_encoded["tail_length"], "max", nil), 50, "5.2a: tail_length max")
|
||||
assert_equal(animation.parameterized_object.constraint_find(comet_encoded["speed"], "max", nil), 25600, "5.2b: speed max")
|
||||
var direction_enum = animation.parameterized_object.constraint_find(comet_encoded["direction"], "enum", nil)
|
||||
@ -244,7 +244,7 @@ var animation_params = {
|
||||
"opacity": {"type": "any", "default": 255},
|
||||
"color": {"default": 0xFFFFFFFF}
|
||||
}
|
||||
var animation_encoded = encode_constraints(animation_params)
|
||||
var animation_encoded = animation.enc_params(animation_params)
|
||||
assert_equal(animation.parameterized_object.constraint_find(animation_encoded["name"], "type", nil), "string", "5.3a: name type")
|
||||
assert_equal(animation.parameterized_object.constraint_find(animation_encoded["name"], "default", nil), "animation", "5.3b: name default")
|
||||
assert_equal(animation.parameterized_object.constraint_find(animation_encoded["loop"], "type", nil), "bool", "5.3c: loop type")
|
||||
@ -256,7 +256,7 @@ var gradient_params = {
|
||||
"gradient_type": {"min": 0, "max": 1, "default": 0},
|
||||
"direction": {"min": 0, "max": 255, "default": 0}
|
||||
}
|
||||
var gradient_encoded = encode_constraints(gradient_params)
|
||||
var gradient_encoded = animation.enc_params(gradient_params)
|
||||
assert_equal(animation.parameterized_object.constraint_mask(gradient_encoded["color"], "nillable"), 0x20, "5.4a: color nillable")
|
||||
assert_equal(animation.parameterized_object.constraint_find(gradient_encoded["color"], "default", 999), nil, "5.4b: color default nil")
|
||||
|
||||
@ -268,7 +268,7 @@ var oscillator_params = {
|
||||
"form": {"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9], "default": 1},
|
||||
"phase": {"min": 0, "max": 100, "default": 0}
|
||||
}
|
||||
var oscillator_encoded = encode_constraints(oscillator_params)
|
||||
var oscillator_encoded = animation.enc_params(oscillator_params)
|
||||
var form_enum = animation.parameterized_object.constraint_find(oscillator_encoded["form"], "enum", nil)
|
||||
assert_array_equal(form_enum, [1, 2, 3, 4, 5, 6, 7, 8, 9], "5.5a: form enum")
|
||||
|
||||
@ -280,7 +280,7 @@ var breathe_params = {
|
||||
"period": {"min": 100, "default": 3000},
|
||||
"curve_factor": {"min": 1, "max": 5, "default": 2}
|
||||
}
|
||||
var breathe_encoded = encode_constraints(breathe_params)
|
||||
var breathe_encoded = animation.enc_params(breathe_params)
|
||||
assert_equal(animation.parameterized_object.constraint_find(breathe_encoded["period"], "min", nil), 100, "5.6a: period min")
|
||||
assert_equal(animation.parameterized_object.constraint_find(breathe_encoded["curve_factor"], "max", nil), 5, "5.6b: curve_factor max")
|
||||
|
||||
@ -291,7 +291,7 @@ print("\n--- Test Group 6: Edge Cases ---")
|
||||
|
||||
# Test 6.1: Empty constraints (only default)
|
||||
var params_6_1 = {"default": 42}
|
||||
var encoded_6_1 = encode_constraints({"test": params_6_1})["test"]
|
||||
var encoded_6_1 = animation.enc_params({"test": params_6_1})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_6_1, "min"), 0x00, "6.1a: no min")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_6_1, "max"), 0x00, "6.1b: no max")
|
||||
assert_equal(animation.parameterized_object.constraint_mask(encoded_6_1, "enum"), 0x00, "6.1c: no enum")
|
||||
@ -299,20 +299,20 @@ assert_equal(animation.parameterized_object.constraint_find(encoded_6_1, "defaul
|
||||
|
||||
# Test 6.2: Zero values
|
||||
var params_6_2 = {"min": 0, "max": 0, "default": 0}
|
||||
var encoded_6_2 = encode_constraints({"test": params_6_2})["test"]
|
||||
var encoded_6_2 = animation.enc_params({"test": params_6_2})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_6_2, "min", nil), 0, "6.2a: zero min")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_6_2, "max", nil), 0, "6.2b: zero max")
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_6_2, "default", nil), 0, "6.2c: zero default")
|
||||
|
||||
# Test 6.3: Single-element enum
|
||||
var params_6_3 = {"enum": [42], "default": 42}
|
||||
var encoded_6_3 = encode_constraints({"test": params_6_3})["test"]
|
||||
var encoded_6_3 = animation.enc_params({"test": params_6_3})["test"]
|
||||
var enum_6_3 = animation.parameterized_object.constraint_find(encoded_6_3, "enum", nil)
|
||||
assert_array_equal(enum_6_3, [42], "6.3a: single-element enum")
|
||||
|
||||
# Test 6.4: Default not found (should return provided default)
|
||||
var params_6_4 = {"min": 0, "max": 100}
|
||||
var encoded_6_4 = encode_constraints({"test": params_6_4})["test"]
|
||||
var encoded_6_4 = animation.enc_params({"test": params_6_4})["test"]
|
||||
assert_equal(animation.parameterized_object.constraint_find(encoded_6_4, "default", 999), 999, "6.4a: missing default returns fallback")
|
||||
|
||||
# Test 6.5: Field not found (should return provided default)
|
||||
|
||||
382
lib/libesp32/berry_animation/src/tests/cpu_metrics_test.be
Normal file
382
lib/libesp32/berry_animation/src/tests/cpu_metrics_test.be
Normal file
@ -0,0 +1,382 @@
|
||||
# CPU Metrics and Profiling Test Suite
|
||||
# Tests for the AnimationEngine CPU metrics and profiling API
|
||||
|
||||
import animation
|
||||
|
||||
print("=== CPU Metrics and Profiling 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
|
||||
|
||||
def assert_greater_than(actual, threshold, message)
|
||||
assert_test(actual > threshold, f"{message} (expected > {threshold}, actual: {actual})")
|
||||
end
|
||||
|
||||
def assert_less_than(actual, threshold, message)
|
||||
assert_test(actual < threshold, f"{message} (expected < {threshold}, actual: {actual})")
|
||||
end
|
||||
|
||||
# Test 1: CPU Metrics Initialization
|
||||
print("\n--- Test 1: CPU Metrics Initialization ---")
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
assert_not_nil(engine, "Engine should be created")
|
||||
assert_equals(engine.tick_count, 0, "Tick count should start at 0")
|
||||
assert_equals(engine.tick_time_sum, 0, "Tick time sum should start at 0")
|
||||
assert_equals(engine.tick_time_min, 999999, "Tick time min should start at max value")
|
||||
assert_equals(engine.tick_time_max, 0, "Tick time max should start at 0")
|
||||
assert_equals(engine.anim_time_sum, 0, "Animation time sum should start at 0")
|
||||
assert_equals(engine.hw_time_sum, 0, "Hardware time sum should start at 0")
|
||||
assert_equals(engine.last_stats_time, 0, "Last stats time should start at 0")
|
||||
assert_equals(engine.stats_period, 5000, "Stats period should be 5000ms")
|
||||
|
||||
# Test 2: Profiling API Initialization
|
||||
print("\n--- Test 2: Profiling API Initialization ---")
|
||||
assert_not_nil(engine.profile_points, "Profile points map should exist")
|
||||
assert_not_nil(engine.profile_start_times, "Profile start times map should exist")
|
||||
assert_equals(size(engine.profile_points), 0, "Profile points should start empty")
|
||||
assert_equals(size(engine.profile_start_times), 0, "Profile start times should start empty")
|
||||
|
||||
# Test 3: Basic Profiling API
|
||||
print("\n--- Test 3: Basic Profiling API ---")
|
||||
|
||||
# Test profile_start
|
||||
engine.profile_start("test_section")
|
||||
assert_equals(size(engine.profile_start_times), 1, "Should have 1 start time after profile_start")
|
||||
assert_not_nil(engine.profile_start_times.find("test_section"), "Start time should be recorded")
|
||||
|
||||
# Test profile_end
|
||||
engine.profile_end("test_section")
|
||||
assert_equals(size(engine.profile_start_times), 0, "Start time should be cleared after profile_end")
|
||||
assert_equals(size(engine.profile_points), 1, "Should have 1 profile point after profile_end")
|
||||
|
||||
var stats = engine.profile_points.find("test_section")
|
||||
assert_not_nil(stats, "Profile stats should exist")
|
||||
assert_equals(stats['count'], 1, "Profile count should be 1")
|
||||
assert_greater_than(stats['sum'], -1, "Profile sum should be non-negative")
|
||||
assert_greater_than(stats['min'], -1, "Profile min should be non-negative")
|
||||
assert_greater_than(stats['max'], -1, "Profile max should be non-negative")
|
||||
|
||||
# Test 4: Multiple Profiling Points
|
||||
print("\n--- Test 4: Multiple Profiling Points ---")
|
||||
|
||||
engine.profile_start("section_a")
|
||||
# Simulate some work
|
||||
var x = 0
|
||||
while x < 10
|
||||
x += 1
|
||||
end
|
||||
engine.profile_end("section_a")
|
||||
|
||||
engine.profile_start("section_b")
|
||||
# Simulate different work
|
||||
var y = 0
|
||||
while y < 5
|
||||
y += 1
|
||||
end
|
||||
engine.profile_end("section_b")
|
||||
|
||||
assert_equals(size(engine.profile_points), 3, "Should have 3 profile points (test_section + section_a + section_b)")
|
||||
assert_not_nil(engine.profile_points.find("section_a"), "section_a stats should exist")
|
||||
assert_not_nil(engine.profile_points.find("section_b"), "section_b stats should exist")
|
||||
|
||||
# Test 5: Repeated Profiling
|
||||
print("\n--- Test 5: Repeated Profiling ---")
|
||||
|
||||
# Profile the same section multiple times
|
||||
for i : 0..9
|
||||
engine.profile_start("repeated_section")
|
||||
var z = 0
|
||||
while z < i
|
||||
z += 1
|
||||
end
|
||||
engine.profile_end("repeated_section")
|
||||
end
|
||||
|
||||
var repeated_stats = engine.profile_points.find("repeated_section")
|
||||
assert_not_nil(repeated_stats, "Repeated section stats should exist")
|
||||
assert_equals(repeated_stats['count'], 10, "Repeated section should have count of 10")
|
||||
assert_greater_than(repeated_stats['sum'], -1, "Repeated section sum should be non-negative")
|
||||
|
||||
# Test 6: Profile End Without Start
|
||||
print("\n--- Test 6: Profile End Without Start ---")
|
||||
|
||||
var points_before = size(engine.profile_points)
|
||||
engine.profile_end("nonexistent_section")
|
||||
var points_after = size(engine.profile_points)
|
||||
|
||||
assert_equals(points_after, points_before, "Profile end without start should not create new point")
|
||||
|
||||
# Test 7: CPU Metrics During Ticks
|
||||
print("\n--- Test 7: CPU Metrics During Ticks ---")
|
||||
|
||||
# Create a fresh engine for tick testing
|
||||
var tick_strip = global.Leds(20)
|
||||
var tick_engine = animation.create_engine(tick_strip)
|
||||
|
||||
# Add a simple animation
|
||||
var test_anim = animation.solid(tick_engine)
|
||||
test_anim.color = 0xFFFF0000
|
||||
tick_engine.add(test_anim)
|
||||
tick_engine.run()
|
||||
|
||||
# Simulate several ticks
|
||||
var current_time = tasmota.millis()
|
||||
for i : 0..9
|
||||
tick_engine.on_tick(current_time + i * 10)
|
||||
end
|
||||
|
||||
# Check that metrics were recorded
|
||||
assert_greater_than(tick_engine.tick_count, 0, "Tick count should increase after ticks")
|
||||
assert_greater_than(tick_engine.tick_time_sum, -1, "Tick time sum should be non-negative")
|
||||
|
||||
# Test 8: Metrics Reset After Stats Period
|
||||
print("\n--- Test 8: Metrics Reset After Stats Period ---")
|
||||
|
||||
# Create engine and simulate ticks over stats period
|
||||
var reset_strip = global.Leds(15)
|
||||
var reset_engine = animation.create_engine(reset_strip)
|
||||
reset_engine.run()
|
||||
|
||||
# Simulate ticks for just under 5 seconds
|
||||
var start_time = 0
|
||||
var current_time = start_time
|
||||
while current_time < 4900
|
||||
reset_engine.on_tick(current_time)
|
||||
current_time += 5
|
||||
end
|
||||
|
||||
var tick_count_before = reset_engine.tick_count
|
||||
assert_greater_than(tick_count_before, 0, "Should have ticks before stats period")
|
||||
|
||||
# Record the last_stats_time before crossing threshold
|
||||
var last_stats_before = reset_engine.last_stats_time
|
||||
|
||||
# Simulate more ticks to cross the 5 second threshold
|
||||
while current_time < 5100
|
||||
reset_engine.on_tick(current_time)
|
||||
current_time += 5
|
||||
end
|
||||
|
||||
# Check that stats were printed (last_stats_time updated)
|
||||
assert_test(reset_engine.last_stats_time > last_stats_before, "Stats should have been printed (last_stats_time updated)")
|
||||
|
||||
# Tick count should have been reset and is now accumulating again
|
||||
# It won't be exactly 1, but should be much smaller than before
|
||||
assert_less_than(reset_engine.tick_count, 50, "Tick count should be small after reset (< 50)")
|
||||
assert_less_than(reset_engine.tick_time_sum, 100, "Tick time sum should be small after reset")
|
||||
|
||||
# Test 9: Profiling with Ticks
|
||||
print("\n--- Test 9: Profiling with Ticks ---")
|
||||
|
||||
var profile_strip = global.Leds(25)
|
||||
var profile_engine = animation.create_engine(profile_strip)
|
||||
profile_engine.run()
|
||||
|
||||
# Simulate ticks with profiling
|
||||
var tick_time = 0
|
||||
for i : 0..19
|
||||
profile_engine.profile_start("tick_work")
|
||||
|
||||
# Simulate some work
|
||||
var work = 0
|
||||
while work < 5
|
||||
work += 1
|
||||
end
|
||||
|
||||
profile_engine.profile_end("tick_work")
|
||||
|
||||
profile_engine.on_tick(tick_time)
|
||||
tick_time += 5
|
||||
end
|
||||
|
||||
var tick_work_stats = profile_engine.profile_points.find("tick_work")
|
||||
assert_not_nil(tick_work_stats, "Tick work profiling should exist")
|
||||
assert_equals(tick_work_stats['count'], 20, "Should have 20 profiled sections")
|
||||
|
||||
# Test 10: Min/Max Tracking
|
||||
print("\n--- Test 10: Min/Max Tracking ---")
|
||||
|
||||
var minmax_strip = global.Leds(10)
|
||||
var minmax_engine = animation.create_engine(minmax_strip)
|
||||
|
||||
# Profile sections with varying durations
|
||||
for i : 0..4
|
||||
minmax_engine.profile_start("varying_work")
|
||||
|
||||
# Variable amount of work
|
||||
var work = 0
|
||||
while work < i * 10
|
||||
work += 1
|
||||
end
|
||||
|
||||
minmax_engine.profile_end("varying_work")
|
||||
end
|
||||
|
||||
var varying_stats = minmax_engine.profile_points.find("varying_work")
|
||||
assert_not_nil(varying_stats, "Varying work stats should exist")
|
||||
assert_test(varying_stats['min'] <= varying_stats['max'], "Min should be <= max")
|
||||
assert_test(varying_stats['min'] >= 0, "Min should be non-negative")
|
||||
assert_test(varying_stats['max'] >= 0, "Max should be non-negative")
|
||||
|
||||
# Test 11: Streaming Statistics Accuracy
|
||||
print("\n--- Test 11: Streaming Statistics Accuracy ---")
|
||||
|
||||
var stats_strip = global.Leds(15)
|
||||
var stats_engine = animation.create_engine(stats_strip)
|
||||
stats_engine.run()
|
||||
|
||||
# Run exactly 10 ticks
|
||||
var stats_time = 0
|
||||
for i : 0..9
|
||||
stats_engine.on_tick(stats_time)
|
||||
stats_time += 5
|
||||
end
|
||||
|
||||
assert_equals(stats_engine.tick_count, 10, "Should have exactly 10 ticks")
|
||||
assert_test(stats_engine.tick_time_sum >= 0, "Tick time sum should be non-negative")
|
||||
assert_test(stats_engine.anim_time_sum >= 0, "Animation time sum should be non-negative")
|
||||
assert_test(stats_engine.hw_time_sum >= 0, "Hardware time sum should be non-negative")
|
||||
|
||||
# Test 12: Profile Points Cleared After Stats
|
||||
print("\n--- Test 12: Profile Points Cleared After Stats ---")
|
||||
|
||||
var clear_strip = global.Leds(20)
|
||||
var clear_engine = animation.create_engine(clear_strip)
|
||||
clear_engine.run()
|
||||
|
||||
# Add some profile points
|
||||
clear_engine.profile_start("temp_section")
|
||||
clear_engine.profile_end("temp_section")
|
||||
|
||||
assert_equals(size(clear_engine.profile_points), 1, "Should have 1 profile point")
|
||||
|
||||
# Simulate ticks to cross stats period
|
||||
var clear_time = 0
|
||||
while clear_time < 5100
|
||||
clear_engine.on_tick(clear_time)
|
||||
clear_time += 5
|
||||
end
|
||||
|
||||
# Profile points should be cleared after stats are printed
|
||||
assert_equals(size(clear_engine.profile_points), 0, "Profile points should be cleared after stats period")
|
||||
|
||||
# Test 13: Multiple Engines Independence
|
||||
print("\n--- Test 13: Multiple Engines Independence ---")
|
||||
|
||||
var strip1 = global.Leds(10)
|
||||
var engine1 = animation.create_engine(strip1)
|
||||
|
||||
var strip2 = global.Leds(20)
|
||||
var engine2 = animation.create_engine(strip2)
|
||||
|
||||
# Profile in engine1
|
||||
engine1.profile_start("engine1_work")
|
||||
engine1.profile_end("engine1_work")
|
||||
|
||||
# Profile in engine2
|
||||
engine2.profile_start("engine2_work")
|
||||
engine2.profile_end("engine2_work")
|
||||
|
||||
assert_equals(size(engine1.profile_points), 1, "Engine1 should have 1 profile point")
|
||||
assert_equals(size(engine2.profile_points), 1, "Engine2 should have 1 profile point")
|
||||
assert_not_nil(engine1.profile_points.find("engine1_work"), "Engine1 should have engine1_work")
|
||||
assert_not_nil(engine2.profile_points.find("engine2_work"), "Engine2 should have engine2_work")
|
||||
assert_test(engine1.profile_points.find("engine2_work") == nil, "Engine1 should not have engine2_work")
|
||||
assert_test(engine2.profile_points.find("engine1_work") == nil, "Engine2 should not have engine1_work")
|
||||
|
||||
# Test 14: Nested Profiling (Same Name)
|
||||
print("\n--- Test 14: Nested Profiling (Same Name) ---")
|
||||
|
||||
var nested_strip = global.Leds(15)
|
||||
var nested_engine = animation.create_engine(nested_strip)
|
||||
|
||||
# Start outer profiling
|
||||
nested_engine.profile_start("nested_section")
|
||||
|
||||
# Start inner profiling (overwrites start time)
|
||||
nested_engine.profile_start("nested_section")
|
||||
|
||||
# End profiling (uses most recent start time)
|
||||
nested_engine.profile_end("nested_section")
|
||||
|
||||
var nested_stats = nested_engine.profile_points.find("nested_section")
|
||||
assert_not_nil(nested_stats, "Nested section stats should exist")
|
||||
assert_equals(nested_stats['count'], 1, "Should have 1 count (inner timing)")
|
||||
|
||||
# Test 15: Performance of Metrics Collection
|
||||
print("\n--- Test 15: Performance of Metrics Collection ---")
|
||||
|
||||
var perf_strip = global.Leds(30)
|
||||
var perf_engine = animation.create_engine(perf_strip)
|
||||
perf_engine.run()
|
||||
|
||||
# Measure overhead of metrics collection
|
||||
var perf_start = tasmota.millis()
|
||||
for i : 0..99
|
||||
perf_engine.on_tick(perf_start + i * 5)
|
||||
end
|
||||
var perf_duration = tasmota.millis() - perf_start
|
||||
|
||||
assert_less_than(perf_duration, 200, f"100 ticks with metrics should be fast (took {perf_duration}ms)")
|
||||
|
||||
# Measure overhead of profiling
|
||||
perf_start = tasmota.millis()
|
||||
for i : 0..99
|
||||
perf_engine.profile_start("perf_test")
|
||||
perf_engine.profile_end("perf_test")
|
||||
end
|
||||
var profile_duration = tasmota.millis() - perf_start
|
||||
|
||||
assert_less_than(profile_duration, 100, f"100 profile calls should be fast (took {profile_duration}ms)")
|
||||
|
||||
# Cleanup
|
||||
engine.stop()
|
||||
tick_engine.stop()
|
||||
reset_engine.stop()
|
||||
profile_engine.stop()
|
||||
clear_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 CPU metrics tests passed!")
|
||||
else
|
||||
print("❌ Some CPU metrics tests failed")
|
||||
raise "test_failed"
|
||||
end
|
||||
|
||||
print("\n=== CPU Metrics Benefits ===")
|
||||
print("CPU Metrics and Profiling features:")
|
||||
print("- Automatic performance tracking every 5 seconds")
|
||||
print("- Separate animation vs hardware timing")
|
||||
print("- Custom profiling API for any code section")
|
||||
print("- Streaming statistics (no array storage)")
|
||||
print("- Memory-efficient for ESP32 embedded systems")
|
||||
print("- Helps identify performance bottlenecks")
|
||||
print("- Min/max/mean timing statistics")
|
||||
@ -97,7 +97,7 @@ def test_successful_compilation()
|
||||
|
||||
berry_code = animation_dsl.compile(sequence_dsl)
|
||||
assert(berry_code != nil, "Should compile sequences with repeat")
|
||||
assert(string.find(berry_code, "animation.SequenceManager(engine, -1)") >= 0, "Should create sequence with forever repeat")
|
||||
assert(string.find(berry_code, "animation.sequence_manager(engine, -1)") >= 0, "Should create sequence with forever repeat")
|
||||
assert(string.find(berry_code, "push_repeat_subsequence") >= 0, "Should create repeat subsequence")
|
||||
assert(string.find(berry_code, "col1_.palette_size") >= 0, "Should reference palette size")
|
||||
assert(string.find(berry_code, "7 + 2") >= 0, "Should preserve arithmetic in repeat count")
|
||||
@ -409,7 +409,7 @@ def test_complete_example()
|
||||
assert(string.find(berry_code, "animation.sawtooth(engine)") >= 0, "Should create sawtooth providers")
|
||||
assert(string.find(berry_code, "animation.pulsating_animation(engine)") >= 0, "Should create animations")
|
||||
assert(string.find(berry_code, 'bytes("FFFF0000"') >= 0, "Should create palette bytes")
|
||||
assert(string.find(berry_code, "animation.SequenceManager(engine") >= 0, "Should create sequences")
|
||||
assert(string.find(berry_code, "animation.sequence_manager(engine") >= 0, "Should create sequences")
|
||||
assert(string.find(berry_code, "push_repeat_subsequence") >= 0, "Should create repeat loops")
|
||||
|
||||
print("✓ Complete example compilation test passed")
|
||||
|
||||
@ -200,7 +200,7 @@ def test_sequence_processing()
|
||||
var berry_code = animation_dsl.compile(basic_seq_dsl)
|
||||
|
||||
assert(berry_code != nil, "Should compile basic sequence")
|
||||
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
|
||||
assert(string.find(berry_code, "var demo_ = animation.sequence_manager(engine)") >= 0, "Should define sequence manager")
|
||||
assert(string.find(berry_code, "red_anim") >= 0, "Should reference animation")
|
||||
assert(string.find(berry_code, ".push_play_step(red_anim_, 2000)") >= 0, "Should create play step")
|
||||
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should add sequence manager")
|
||||
@ -224,7 +224,7 @@ def test_sequence_processing()
|
||||
# print(berry_code)
|
||||
# print("==================================================")
|
||||
assert(berry_code != nil, "Should compile repeat sequence")
|
||||
assert(string.find(berry_code, "animation.SequenceManager(engine, 3)") >= 0, "Should generate repeat subsequence")
|
||||
assert(string.find(berry_code, "animation.sequence_manager(engine, 3)") >= 0, "Should generate repeat subsequence")
|
||||
assert(string.find(berry_code, ".push_wait_step(500)") >= 0, "Should generate wait step")
|
||||
|
||||
print("✓ Sequence processing test passed")
|
||||
|
||||
@ -0,0 +1,669 @@
|
||||
# DSL Template Animation Test
|
||||
# Tests the new template animation feature that generates classes extending engine_proxy
|
||||
#
|
||||
# This test suite covers:
|
||||
# 1. Basic template animation compilation
|
||||
# 2. Parameter constraints (min, max, default)
|
||||
# 3. Parameter reference as self.param
|
||||
# 4. Closure wrapping for parameters
|
||||
# 5. PARAMS generation with encode_constraints
|
||||
# 6. Multiple instances of template animations
|
||||
# 7. Template animation with sequences
|
||||
# 8. Parameter usage in different contexts
|
||||
|
||||
import animation
|
||||
import animation_dsl
|
||||
import string
|
||||
|
||||
# Test class to verify template animation functionality
|
||||
class DSLTemplateAnimationTest
|
||||
var test_results
|
||||
|
||||
def init()
|
||||
self.test_results = []
|
||||
end
|
||||
|
||||
# Helper method to run a test case
|
||||
def run_test(test_name, test_func)
|
||||
try
|
||||
test_func()
|
||||
self.test_results.push(f"✓ {test_name}")
|
||||
return true
|
||||
except .. as e, msg
|
||||
self.test_results.push(f"✗ {test_name}: {msg}")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Test basic template animation compilation
|
||||
def test_basic_template_animation()
|
||||
var dsl_code =
|
||||
"template animation simple_effect {\n" +
|
||||
" param my_color type color\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=my_color)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Basic template animation should compile successfully"
|
||||
end
|
||||
|
||||
# Check that it generates a class, not a function
|
||||
if string.find(berry_code, "class simple_effect_animation : animation.engine_proxy") == -1
|
||||
raise "generation_error", "Should generate class extending engine_proxy"
|
||||
end
|
||||
|
||||
# Check for PARAMS static variable
|
||||
if string.find(berry_code, "static var PARAMS = animation.enc_params({") == -1
|
||||
raise "generation_error", "Should generate PARAMS with encode_constraints"
|
||||
end
|
||||
|
||||
# Check for setup_template method (replaces init method)
|
||||
if string.find(berry_code, "def setup_template()") == -1
|
||||
raise "generation_error", "Should generate setup_template method"
|
||||
end
|
||||
|
||||
# Check for engine variable assignment
|
||||
if string.find(berry_code, "var engine = self ") == -1
|
||||
raise "generation_error", "Should assign engine from self.engine"
|
||||
end
|
||||
|
||||
# Check for self.add instead of engine.add
|
||||
if string.find(berry_code, "self.add(test_)") == -1
|
||||
raise "generation_error", "Should use self.add instead of engine.add"
|
||||
end
|
||||
end
|
||||
|
||||
# Test parameter constraints (min, max, default, nillable)
|
||||
def test_parameter_constraints()
|
||||
var dsl_code =
|
||||
"template animation constrained_effect {\n" +
|
||||
" param duration type time min 0 max 3600 default 5\n" +
|
||||
" param intensity type number min 0 max 255 default 128 nillable true\n" +
|
||||
" param colors type palette nillable false\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=red)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with constraints should compile successfully"
|
||||
end
|
||||
|
||||
# Check that constraints are in PARAMS
|
||||
if string.find(berry_code, '"duration": {"type": "time", "min": 0, "max": 3600, "default": 5}') == -1
|
||||
raise "generation_error", "Duration constraints should be in PARAMS"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"intensity": {"type": "number", "min": 0, "max": 255, "default": 128, "nillable": true}') == -1
|
||||
raise "generation_error", "Intensity constraints with nillable should be in PARAMS"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"colors": {"type": "palette", "nillable": false}') == -1
|
||||
raise "generation_error", "Colors with nillable false should be in PARAMS"
|
||||
end
|
||||
end
|
||||
|
||||
# Test parameter reference as self.param
|
||||
def test_self_param_reference()
|
||||
var dsl_code =
|
||||
"template animation param_ref_effect {\n" +
|
||||
" param my_color type palette\n" +
|
||||
" param my_duration type time\n" +
|
||||
" \n" +
|
||||
" color col = color_cycle(palette=my_color, cycle_period=0)\n" +
|
||||
" animation test = pulsating_animation(color=col, period=my_duration)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with parameter references should compile successfully"
|
||||
end
|
||||
|
||||
# Check that palette parameter is wrapped with self.my_color
|
||||
if string.find(berry_code, "self.my_color") == -1
|
||||
raise "generation_error", "Palette parameter should be referenced as self.my_color"
|
||||
end
|
||||
|
||||
# Check that it's wrapped in create_closure_value
|
||||
if string.find(berry_code, "animation.create_closure_value(engine, def (engine) return self.my_color end)") == -1
|
||||
raise "generation_error", "Palette parameter should be wrapped in create_closure_value"
|
||||
end
|
||||
end
|
||||
|
||||
# Test parameter in computed expressions
|
||||
def test_param_in_computed_expression()
|
||||
var dsl_code =
|
||||
"template animation computed_effect {\n" +
|
||||
" param base_size type number\n" +
|
||||
" \n" +
|
||||
" set strip_len = strip_length()\n" +
|
||||
" set computed_size = strip_len / 2\n" +
|
||||
" \n" +
|
||||
" animation test = beacon_animation(beacon_size=base_size)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with computed expressions should compile successfully"
|
||||
end
|
||||
|
||||
# Check that parameter is used correctly
|
||||
if string.find(berry_code, "self.base_size") == -1
|
||||
raise "generation_error", "Parameter should be referenced as self.base_size"
|
||||
end
|
||||
end
|
||||
|
||||
# Test template animation with sequence
|
||||
def test_template_animation_with_sequence()
|
||||
var dsl_code =
|
||||
"template animation sequence_effect {\n" +
|
||||
" param colors type palette\n" +
|
||||
" param duration type time\n" +
|
||||
" \n" +
|
||||
" color col = color_cycle(palette=colors, cycle_period=0)\n" +
|
||||
" animation anim = solid(color=col)\n" +
|
||||
" \n" +
|
||||
" sequence seq repeat forever {\n" +
|
||||
" play anim for duration\n" +
|
||||
" col.next = 1\n" +
|
||||
" }\n" +
|
||||
" \n" +
|
||||
" run seq\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with sequence should compile successfully"
|
||||
end
|
||||
|
||||
# Check that duration parameter is used in play statement (wrapped in closure)
|
||||
if string.find(berry_code, "def (engine) return self.duration end") == -1
|
||||
raise "generation_error", "Duration parameter should be wrapped in closure for play statement"
|
||||
end
|
||||
|
||||
# Check that sequence is added with self.add
|
||||
if string.find(berry_code, "self.add(seq_)") == -1
|
||||
raise "generation_error", "Sequence should be added with self.add"
|
||||
end
|
||||
end
|
||||
|
||||
# Test template animation with value provider parameter
|
||||
def test_template_animation_with_value_provider()
|
||||
var dsl_code =
|
||||
"template animation provider_effect {\n" +
|
||||
" param duration type time\n" +
|
||||
" \n" +
|
||||
" set strip_len = strip_length()\n" +
|
||||
" set oscillator = sawtooth(min_value=0, max_value=strip_len, duration=duration)\n" +
|
||||
" \n" +
|
||||
" animation test = beacon_animation(pos=oscillator)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with value provider should compile successfully"
|
||||
end
|
||||
|
||||
# Check that duration is used in the value provider
|
||||
if string.find(berry_code, "provider.duration = animation.create_closure_value(engine, def (engine) return self.duration end)") == -1
|
||||
raise "generation_error", "Duration parameter should be wrapped in closure for value provider"
|
||||
end
|
||||
end
|
||||
|
||||
# Test template animation with multiple parameters
|
||||
def test_multiple_parameters()
|
||||
var dsl_code =
|
||||
"template animation multi_param_effect {\n" +
|
||||
" param colors type palette\n" +
|
||||
" param duration type time min 1 max 10 default 5\n" +
|
||||
" param intensity type number min 0 max 255 default 200\n" +
|
||||
" \n" +
|
||||
" color col = color_cycle(palette=colors, cycle_period=0)\n" +
|
||||
" animation test = solid(color=col)\n" +
|
||||
" test.opacity = intensity\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with multiple parameters should compile successfully"
|
||||
end
|
||||
|
||||
# Check all parameters are in PARAMS
|
||||
if string.find(berry_code, '"colors": {"type": "palette"}') == -1
|
||||
raise "generation_error", "Colors parameter should be in PARAMS"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"duration": {"type": "time", "min": 1, "max": 10, "default": 5}') == -1
|
||||
raise "generation_error", "Duration parameter with constraints should be in PARAMS"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"intensity": {"type": "number", "min": 0, "max": 255, "default": 200}') == -1
|
||||
raise "generation_error", "Intensity parameter with constraints should be in PARAMS"
|
||||
end
|
||||
|
||||
# Check that intensity parameter is used correctly
|
||||
if string.find(berry_code, "self.intensity") == -1
|
||||
raise "generation_error", "Intensity parameter should be referenced as self.intensity"
|
||||
end
|
||||
end
|
||||
|
||||
# Test template animation with no parameters
|
||||
def test_no_parameters()
|
||||
var dsl_code =
|
||||
"template animation no_param_effect {\n" +
|
||||
" animation test = solid(color=red)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with no parameters should compile successfully"
|
||||
end
|
||||
|
||||
# Check that PARAMS is present with encode_constraints (may have whitespace)
|
||||
if string.find(berry_code, "static var PARAMS = animation.enc_params({") == -1
|
||||
raise "generation_error", "PARAMS should be present with encode_constraints"
|
||||
end
|
||||
|
||||
# Check that PARAMS has closing brace (empty constraints)
|
||||
if string.find(berry_code, "animation.enc_params({") == -1
|
||||
raise "generation_error", "PARAMS should use encode_constraints with empty map"
|
||||
end
|
||||
end
|
||||
|
||||
# Test parameter usage validation (should not warn for used parameters)
|
||||
def test_parameter_usage_validation()
|
||||
var dsl_code =
|
||||
"template animation used_params_effect {\n" +
|
||||
" param colors type palette\n" +
|
||||
" param duration type time\n" +
|
||||
" \n" +
|
||||
" color col = color_cycle(palette=colors, cycle_period=0)\n" +
|
||||
" animation test = solid(color=col)\n" +
|
||||
" \n" +
|
||||
" sequence seq repeat forever {\n" +
|
||||
" play test for duration\n" +
|
||||
" }\n" +
|
||||
" \n" +
|
||||
" run seq\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with used parameters should compile successfully"
|
||||
end
|
||||
|
||||
# Check that there are no warnings about unused parameters
|
||||
# (warnings would appear as comments in the generated code)
|
||||
var has_unused_warning = string.find(berry_code, "parameter 'colors' is declared but never used") != -1 ||
|
||||
string.find(berry_code, "parameter 'duration' is declared but never used") != -1
|
||||
|
||||
if has_unused_warning
|
||||
raise "validation_error", "Should not warn about parameters that are actually used"
|
||||
end
|
||||
end
|
||||
|
||||
# Test color defaults (named colors and hex colors)
|
||||
def test_color_defaults()
|
||||
var dsl_code =
|
||||
"template animation color_defaults_effect {\n" +
|
||||
" param color1 type color default red\n" +
|
||||
" param color2 type color default 0xFF00FF00\n" +
|
||||
" param color3 type color default transparent\n" +
|
||||
" param color4 type color default blue\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=color1)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with color defaults should compile successfully"
|
||||
end
|
||||
|
||||
# Check that named colors are converted to ARGB hex values
|
||||
if string.find(berry_code, '"color1": {"type": "color", "default": 0xFFFF0000}') == -1
|
||||
raise "generation_error", "Named color 'red' should be converted to 0xFFFF0000"
|
||||
end
|
||||
|
||||
# Check that hex colors are preserved
|
||||
if string.find(berry_code, '"color2": {"type": "color", "default": 0xFF00FF00}') == -1
|
||||
raise "generation_error", "Hex color should be preserved as 0xFF00FF00"
|
||||
end
|
||||
|
||||
# Check that transparent is converted correctly
|
||||
if string.find(berry_code, '"color3": {"type": "color", "default": 0x00000000}') == -1
|
||||
raise "generation_error", "Named color 'transparent' should be converted to 0x00000000"
|
||||
end
|
||||
|
||||
# Check that blue is converted correctly
|
||||
if string.find(berry_code, '"color4": {"type": "color", "default": 0xFF0000FF}') == -1
|
||||
raise "generation_error", "Named color 'blue' should be converted to 0xFF0000FF"
|
||||
end
|
||||
end
|
||||
|
||||
# Test time defaults (seconds, minutes, hours)
|
||||
def test_time_defaults()
|
||||
var dsl_code =
|
||||
"template animation time_defaults_effect {\n" +
|
||||
" param duration1 type time default 5s\n" +
|
||||
" param duration2 type time default 2m\n" +
|
||||
" param duration3 type time default 1h\n" +
|
||||
" param duration4 type time default 500ms\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=red)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with time defaults should compile successfully"
|
||||
end
|
||||
|
||||
# Check that time values are converted to milliseconds
|
||||
if string.find(berry_code, '"duration1": {"type": "time", "default": 5000}') == -1
|
||||
raise "generation_error", "Time '5s' should be converted to 5000 milliseconds"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"duration2": {"type": "time", "default": 120000}') == -1
|
||||
raise "generation_error", "Time '2m' should be converted to 120000 milliseconds"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"duration3": {"type": "time", "default": 3600000}') == -1
|
||||
raise "generation_error", "Time '1h' should be converted to 3600000 milliseconds"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"duration4": {"type": "time", "default": 500}') == -1
|
||||
raise "generation_error", "Time '500ms' should be converted to 500 milliseconds"
|
||||
end
|
||||
end
|
||||
|
||||
# Test time constraints for min/max
|
||||
def test_time_constraints()
|
||||
var dsl_code =
|
||||
"template animation time_constraints_effect {\n" +
|
||||
" param duration type time min 1s max 1h default 10s\n" +
|
||||
" param speed type time min 100ms max 5s default 1s\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=red)\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with time constraints should compile successfully"
|
||||
end
|
||||
|
||||
# Check that time constraints are converted to milliseconds
|
||||
if string.find(berry_code, '"duration": {"type": "time", "min": 1000, "max": 3600000, "default": 10000}') == -1
|
||||
raise "generation_error", "Time constraints should be converted to milliseconds (1s=1000, 1h=3600000, 10s=10000)"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"speed": {"type": "time", "min": 100, "max": 5000, "default": 1000}') == -1
|
||||
raise "generation_error", "Time constraints should be converted to milliseconds (100ms=100, 5s=5000, 1s=1000)"
|
||||
end
|
||||
end
|
||||
|
||||
# Test mixed constraint types (colors, times, numbers)
|
||||
def test_mixed_constraint_types()
|
||||
var dsl_code =
|
||||
"template animation mixed_constraints_effect {\n" +
|
||||
" param eye_color type color default red\n" +
|
||||
" param back_color type color default transparent\n" +
|
||||
" param duration type time min 0 max 1h default 10s\n" +
|
||||
" param brightness type number min 0 max 255 default 200\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=eye_color)\n" +
|
||||
" test.opacity = brightness\n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with mixed constraint types should compile successfully"
|
||||
end
|
||||
|
||||
# Check color defaults
|
||||
if string.find(berry_code, '"eye_color": {"type": "color", "default": 0xFFFF0000}') == -1
|
||||
raise "generation_error", "Color default 'red' should be converted to 0xFFFF0000"
|
||||
end
|
||||
|
||||
if string.find(berry_code, '"back_color": {"type": "color", "default": 0x00000000}') == -1
|
||||
raise "generation_error", "Color default 'transparent' should be converted to 0x00000000"
|
||||
end
|
||||
|
||||
# Check time constraints
|
||||
if string.find(berry_code, '"duration": {"type": "time", "min": 0, "max": 3600000, "default": 10000}') == -1
|
||||
raise "generation_error", "Time constraints should be converted to milliseconds"
|
||||
end
|
||||
|
||||
# Check number constraints
|
||||
if string.find(berry_code, '"brightness": {"type": "number", "min": 0, "max": 255, "default": 200}') == -1
|
||||
raise "generation_error", "Number constraints should be preserved"
|
||||
end
|
||||
end
|
||||
|
||||
# Test inherited parameters from engine_proxy class hierarchy
|
||||
def test_inherited_parameters()
|
||||
var dsl_code =
|
||||
"template animation inherited_params_effect {\n" +
|
||||
" param colors type palette\n" +
|
||||
" param period type time default 5s\n" +
|
||||
" \n" +
|
||||
" set strip_len = strip_length()\n" +
|
||||
" set shutter_size = sawtooth(min_value=0, max_value=strip_len, duration=duration)\n" +
|
||||
" \n" +
|
||||
" color col = color_cycle(palette=colors, cycle_period=0)\n" +
|
||||
" animation test = beacon_animation(color=col, beacon_size=shutter_size)\n" +
|
||||
" \n" +
|
||||
" sequence seq repeat forever {\n" +
|
||||
" play test for period\n" +
|
||||
" }\n" +
|
||||
" \n" +
|
||||
" run seq\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation with inherited parameters should compile successfully"
|
||||
end
|
||||
|
||||
# Check that 'duration' parameter (inherited from engine_proxy) is recognized
|
||||
# It should be wrapped in a closure when used in the value provider
|
||||
if string.find(berry_code, "self.duration") == -1
|
||||
raise "generation_error", "Inherited parameter 'duration' should be referenced as self.duration"
|
||||
end
|
||||
|
||||
# Check that duration is wrapped in create_closure_value
|
||||
if string.find(berry_code, "animation.create_closure_value(engine, def (engine) return self.duration end)") == -1
|
||||
raise "generation_error", "Inherited parameter 'duration' should be wrapped in closure"
|
||||
end
|
||||
|
||||
# Verify that other inherited parameters would also work (name, priority, opacity, color, loop, is_running)
|
||||
# These are all valid parameters from the engine_proxy class hierarchy
|
||||
end
|
||||
|
||||
# Test all inherited parameters from engine_proxy
|
||||
def test_all_inherited_parameters()
|
||||
var dsl_code =
|
||||
"template animation all_inherited_effect {\n" +
|
||||
" param my_color type color\n" +
|
||||
" \n" +
|
||||
" animation test = solid(color=my_color)\n" +
|
||||
" test.opacity = opacity\n" +
|
||||
" test.priority = priority\n" +
|
||||
" test.duration = duration\n" +
|
||||
" test.loop = loop\n" +
|
||||
" \n" +
|
||||
" run test\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Template animation using all inherited parameters should compile successfully"
|
||||
end
|
||||
|
||||
# Check that all inherited parameters are recognized and referenced as self.param
|
||||
var inherited_params = ["opacity", "priority", "duration", "loop"]
|
||||
for param : inherited_params
|
||||
if string.find(berry_code, f"self.{param}") == -1
|
||||
raise "generation_error", f"Inherited parameter '{param}' should be referenced as self.{param}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Test complex template animation with all features
|
||||
def test_complex_template_animation()
|
||||
var dsl_code =
|
||||
"template animation complex_shutter {\n" +
|
||||
" param colors type palette\n" +
|
||||
" param duration type time min 0 max 3600 default 5\n" +
|
||||
" \n" +
|
||||
" set strip_len = strip_length()\n" +
|
||||
" set strip_len2 = (strip_len + 1) / 2\n" +
|
||||
" set shutter_size = sawtooth(min_value=0, max_value=strip_len, duration=duration)\n" +
|
||||
" \n" +
|
||||
" color col1 = color_cycle(palette=colors, cycle_period=0)\n" +
|
||||
" color col2 = color_cycle(palette=colors, cycle_period=0)\n" +
|
||||
" col2.next = 1\n" +
|
||||
" \n" +
|
||||
" animation shutter = beacon_animation(\n" +
|
||||
" color=col1\n" +
|
||||
" back_color=col2\n" +
|
||||
" pos=strip_len2\n" +
|
||||
" beacon_size=shutter_size\n" +
|
||||
" )\n" +
|
||||
" \n" +
|
||||
" sequence seq repeat forever {\n" +
|
||||
" restart shutter_size\n" +
|
||||
" play shutter for duration\n" +
|
||||
" col1.next = 1\n" +
|
||||
" }\n" +
|
||||
" \n" +
|
||||
" run seq\n" +
|
||||
"}\n"
|
||||
|
||||
var berry_code = animation_dsl.compile_dsl(dsl_code)
|
||||
|
||||
if berry_code == nil
|
||||
raise "compilation_error", "Complex template animation should compile successfully"
|
||||
end
|
||||
|
||||
# Check class structure
|
||||
if string.find(berry_code, "class complex_shutter_animation : animation.engine_proxy") == -1
|
||||
raise "generation_error", "Should generate class with correct name"
|
||||
end
|
||||
|
||||
# Check PARAMS with constraints
|
||||
if string.find(berry_code, '"duration": {"type": "time", "min": 0, "max": 3600, "default": 5}') == -1
|
||||
raise "generation_error", "Duration constraints should be in PARAMS"
|
||||
end
|
||||
|
||||
# Check parameter references
|
||||
if string.find(berry_code, "self.colors") == -1
|
||||
raise "generation_error", "Colors parameter should be referenced as self.colors"
|
||||
end
|
||||
|
||||
if string.find(berry_code, "self.duration") == -1
|
||||
raise "generation_error", "Duration parameter should be referenced as self.duration"
|
||||
end
|
||||
|
||||
# Check closure wrapping
|
||||
if string.find(berry_code, "animation.create_closure_value(engine, def (engine) return self.colors end)") == -1
|
||||
raise "generation_error", "Colors parameter should be wrapped in closure"
|
||||
end
|
||||
|
||||
# Check closure wrapping in sequence for duration parameter
|
||||
if string.find(berry_code, "def (engine) return self.duration end") == -1
|
||||
raise "generation_error", "Duration should be wrapped in closure for play statement"
|
||||
end
|
||||
|
||||
# Check self.add
|
||||
if string.find(berry_code, "self.add(seq_)") == -1
|
||||
raise "generation_error", "Should use self.add for sequence"
|
||||
end
|
||||
end
|
||||
|
||||
# Run all tests
|
||||
def run_all_tests()
|
||||
print("Running DSL Template Animation Tests...")
|
||||
|
||||
var total_tests = 0
|
||||
var passed_tests = 0
|
||||
|
||||
# Test cases
|
||||
var tests = [
|
||||
["Basic Template Animation", / -> self.test_basic_template_animation()],
|
||||
["Parameter Constraints", / -> self.test_parameter_constraints()],
|
||||
["Self Parameter Reference", / -> self.test_self_param_reference()],
|
||||
["Parameter in Computed Expression", / -> self.test_param_in_computed_expression()],
|
||||
["Template Animation with Sequence", / -> self.test_template_animation_with_sequence()],
|
||||
["Template Animation with Value Provider", / -> self.test_template_animation_with_value_provider()],
|
||||
["Multiple Parameters", / -> self.test_multiple_parameters()],
|
||||
["No Parameters", / -> self.test_no_parameters()],
|
||||
["Parameter Usage Validation", / -> self.test_parameter_usage_validation()],
|
||||
["Color Defaults", / -> self.test_color_defaults()],
|
||||
["Time Defaults", / -> self.test_time_defaults()],
|
||||
["Time Constraints", / -> self.test_time_constraints()],
|
||||
["Mixed Constraint Types", / -> self.test_mixed_constraint_types()],
|
||||
["Inherited Parameters", / -> self.test_inherited_parameters()],
|
||||
["All Inherited Parameters", / -> self.test_all_inherited_parameters()],
|
||||
["Complex Template Animation", / -> self.test_complex_template_animation()]
|
||||
]
|
||||
|
||||
for test : tests
|
||||
total_tests += 1
|
||||
if self.run_test(test[0], test[1])
|
||||
passed_tests += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Print results
|
||||
print(f"\nTest Results:")
|
||||
for result : self.test_results
|
||||
print(f" {result}")
|
||||
end
|
||||
|
||||
print(f"\nSummary: {passed_tests}/{total_tests} tests passed")
|
||||
|
||||
if passed_tests == total_tests
|
||||
print("✓ All DSL template animation tests passed!")
|
||||
return true
|
||||
else
|
||||
print("✗ Some DSL template animation tests failed!")
|
||||
raise "test_failed"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Run tests
|
||||
var test_runner = DSLTemplateAnimationTest()
|
||||
test_runner.run_all_tests()
|
||||
|
||||
# Export for use in other test files
|
||||
return {
|
||||
"DSLTemplateAnimationTest": DSLTemplateAnimationTest
|
||||
}
|
||||
@ -47,7 +47,7 @@ def test_basic_transpilation()
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "var engine = animation.init_strip()") >= 0, "Should generate strip configuration")
|
||||
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should generate color definition")
|
||||
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should generate sequence manager")
|
||||
assert(string.find(berry_code, "var demo_ = animation.sequence_manager(engine)") >= 0, "Should generate sequence manager")
|
||||
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should add sequence manager")
|
||||
|
||||
# print("Generated Berry code:")
|
||||
@ -175,7 +175,7 @@ def test_sequences()
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
assert(berry_code != nil, "Should compile sequence")
|
||||
assert(string.find(berry_code, "var test_seq_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
|
||||
assert(string.find(berry_code, "var test_seq_ = animation.sequence_manager(engine)") >= 0, "Should define sequence manager")
|
||||
assert(string.find(berry_code, ".push_play_step(") >= 0, "Should add play step")
|
||||
assert(string.find(berry_code, "3000)") >= 0, "Should reference duration")
|
||||
assert(string.find(berry_code, "engine.run()") >= 0, "Should start engine")
|
||||
@ -203,7 +203,7 @@ def test_sequence_assignments()
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
assert(berry_code != nil, "Should compile sequence with assignments")
|
||||
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
|
||||
assert(string.find(berry_code, "var demo_ = animation.sequence_manager(engine)") >= 0, "Should define sequence manager")
|
||||
assert(string.find(berry_code, ".push_closure_step") >= 0, "Should generate closure step")
|
||||
assert(string.find(berry_code, "test_.opacity = brightness_") >= 0, "Should generate assignment")
|
||||
|
||||
@ -710,7 +710,7 @@ def test_complex_dsl()
|
||||
# Check for key components
|
||||
assert(string.find(berry_code, "var engine = animation.init_strip()") >= 0, "Should have default strip initialization")
|
||||
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should have color definitions")
|
||||
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should have sequence definition")
|
||||
assert(string.find(berry_code, "var demo_ = animation.sequence_manager(engine)") >= 0, "Should have sequence definition")
|
||||
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should have execution")
|
||||
|
||||
print("Generated code structure looks correct")
|
||||
@ -1120,7 +1120,7 @@ def test_invalid_sequence_commands()
|
||||
|
||||
var result4 = animation_dsl.compile(valid_sequence_dsl)
|
||||
assert(result4 != nil, "Should compile valid sequence successfully")
|
||||
assert(string.find(result4, "SequenceManager") >= 0, "Should generate sequence manager")
|
||||
assert(string.find(result4, "sequence_manager") >= 0, "Should generate sequence manager")
|
||||
assert(string.find(result4, "push_play_step") >= 0, "Should generate play step")
|
||||
assert(string.find(result4, "push_wait_step") >= 0, "Should generate wait step")
|
||||
assert(string.find(result4, "log(f\"test message\", 3)") >= 0, "Should generate log statement")
|
||||
|
||||
141
lib/libesp32/berry_animation/src/tests/engine_proxy_test.be
Normal file
141
lib/libesp32/berry_animation/src/tests/engine_proxy_test.be
Normal file
@ -0,0 +1,141 @@
|
||||
# Unit tests for EngineProxy class
|
||||
#
|
||||
# Tests the ability to create animations that combine both rendering
|
||||
# and orchestration of sub-animations and sequences.
|
||||
|
||||
import animation
|
||||
|
||||
print("Starting EngineProxy Tests...")
|
||||
|
||||
# Create test engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Test 1: Basic creation
|
||||
print("\n=== Test 1: Basic Creation ===")
|
||||
var proxy = animation.engine_proxy(engine)
|
||||
assert(proxy != nil, "Engine proxy should be created")
|
||||
assert(isinstance(proxy, animation.playable), "Engine proxy should be a Playable")
|
||||
assert(isinstance(proxy, animation.animation), "Engine proxy should be an Animation")
|
||||
assert(proxy.is_running == false, "Engine proxy should not be running initially")
|
||||
print("✓ Basic creation test passed")
|
||||
|
||||
# Test 2: Add child animations
|
||||
print("\n=== Test 2: Add Child Animations ===")
|
||||
var child1 = animation.solid(engine)
|
||||
child1.color = 0xFFFF0000 # Red
|
||||
child1.name = "red_child"
|
||||
|
||||
var child2 = animation.solid(engine)
|
||||
child2.color = 0xFF00FF00 # Green
|
||||
child2.name = "green_child"
|
||||
|
||||
proxy.add(child1)
|
||||
proxy.add(child2)
|
||||
assert(size(proxy.animations) == 2, "Should have 2 animations")
|
||||
print("✓ Add child animations test passed")
|
||||
|
||||
# Test 3: Add child sequence
|
||||
print("\n=== Test 3: Add Child Sequence ===")
|
||||
var seq = animation.sequence_manager(engine, 1)
|
||||
seq.push_play_step(child1, 1000)
|
||||
seq.push_wait_step(500)
|
||||
seq.push_play_step(child2, 1000)
|
||||
|
||||
proxy.add(seq)
|
||||
assert(size(proxy.animations) == 2, "Should have 2 animations total")
|
||||
assert(size(proxy.sequences) == 1, "Should have 1 sequence")
|
||||
print("✓ Add child sequence test passed")
|
||||
|
||||
# Test 4: Start engine proxy (should start all animations)
|
||||
print("\n=== Test 4: Start Engine Proxy ===")
|
||||
engine.time_ms = 1000
|
||||
proxy.start(engine.time_ms)
|
||||
assert(proxy.is_running == true, "Engine proxy should be running")
|
||||
assert(child1.is_running == true, "Child1 should be running")
|
||||
assert(child2.is_running == true, "Child2 should be running")
|
||||
assert(seq.is_running == true, "Sequence should be running")
|
||||
print("✓ Start engine proxy test passed")
|
||||
|
||||
# Test 5: Update engine proxy (should update all animations)
|
||||
print("\n=== Test 5: Update Engine Proxy ===")
|
||||
engine.time_ms = 1500
|
||||
var result = proxy.update(engine.time_ms)
|
||||
assert(result == true, "Engine proxy should still be running")
|
||||
print("✓ Update engine proxy test passed")
|
||||
|
||||
# Test 6: Render engine proxy
|
||||
print("\n=== Test 6: Render Engine Proxy ===")
|
||||
var frame = animation.frame_buffer(30)
|
||||
engine.time_ms = 2000
|
||||
result = proxy.render(frame, engine.time_ms)
|
||||
# Rendering should work (may or may not modify frame depending on animations)
|
||||
print("✓ Render engine proxy test passed")
|
||||
|
||||
# Test 7: Stop engine proxy (should stop all animations)
|
||||
print("\n=== Test 7: Stop Engine Proxy ===")
|
||||
proxy.stop()
|
||||
assert(proxy.is_running == false, "Engine proxy should be stopped")
|
||||
assert(child1.is_running == false, "Child1 should be stopped")
|
||||
assert(child2.is_running == false, "Child2 should be stopped")
|
||||
assert(seq.is_running == false, "Sequence should be stopped")
|
||||
print("✓ Stop engine proxy test passed")
|
||||
|
||||
# Test 8: Remove child
|
||||
print("\n=== Test 8: Remove Child ===")
|
||||
proxy.remove(child1)
|
||||
assert(size(proxy.animations) == 1, "Should have 1 animations after removal")
|
||||
proxy.remove(seq)
|
||||
assert(size(proxy.animations) == 1, "Should have 1 child after sequence removal")
|
||||
assert(size(proxy.sequences) == 0, "Should have 0 sequences after removal")
|
||||
print("✓ Remove child test passed")
|
||||
|
||||
# Test 9: Engine proxy with own rendering
|
||||
print("\n=== Test 9: Engine Proxy with Own Rendering ===")
|
||||
var proxy2 = animation.engine_proxy(engine)
|
||||
proxy2.color = 0xFF0000FF # Blue background
|
||||
proxy2.name = "blue_proxy"
|
||||
|
||||
var pulse = animation.breathe_animation(engine)
|
||||
pulse.color = 0xFFFFFF00 # Yellow
|
||||
pulse.period = 2000
|
||||
pulse.name = "yellow_pulse"
|
||||
|
||||
proxy2.add(pulse)
|
||||
engine.time_ms = 3000
|
||||
proxy2.start(engine.time_ms)
|
||||
|
||||
var frame2 = animation.frame_buffer(30)
|
||||
result = proxy2.render(frame2, engine.time_ms)
|
||||
assert(result == true, "Engine proxy with own rendering should modify frame")
|
||||
print("✓ Engine proxy with own rendering test passed")
|
||||
|
||||
# Test 10: Engine integration
|
||||
print("\n=== Test 10: Engine Integration ===")
|
||||
var proxy3 = animation.engine_proxy(engine)
|
||||
proxy3.color = 0xFFFF00FF # Magenta
|
||||
proxy3.priority = 15
|
||||
proxy3.name = "engine_proxy"
|
||||
|
||||
# Add to engine (should work since EngineProxy is a Playable)
|
||||
engine.add(proxy3)
|
||||
assert(size(engine.get_animations()) == 1, "Engine should have 1 animation")
|
||||
print("✓ Engine integration test passed")
|
||||
|
||||
# Test 11: Type checking
|
||||
print("\n=== Test 11: Type Checking ===")
|
||||
assert(isinstance(proxy, animation.playable), "Engine proxy is a Playable")
|
||||
assert(isinstance(proxy, animation.animation), "Engine proxy is an Animation")
|
||||
assert(!isinstance(proxy, animation.sequence_manager), "Engine proxy is not a SequenceManager")
|
||||
print("✓ Type checking test passed")
|
||||
|
||||
# Test 12: String representation
|
||||
print("\n=== Test 12: String Representation ===")
|
||||
var str_repr = str(proxy2)
|
||||
assert(str_repr != nil, "String representation should exist")
|
||||
print(f"Engine proxy string: {str_repr}")
|
||||
print("✓ String representation test passed")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🎉 All EngineProxy tests passed!")
|
||||
print("="*50)
|
||||
@ -206,7 +206,6 @@ def test_animation_engine_event_integration()
|
||||
|
||||
# Test interrupt methods exist
|
||||
return introspect.contains(engine, "interrupt_current") &&
|
||||
introspect.contains(engine, "interrupt_all") &&
|
||||
introspect.contains(engine, "resume")
|
||||
end
|
||||
|
||||
|
||||
@ -43,6 +43,10 @@ def test_get_param_value_with_color_provider()
|
||||
self.produce_value_called += 1
|
||||
return self.color
|
||||
end
|
||||
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
var tracking_provider = TrackingColorProvider(mock_engine, 0xFF00FF00) # Green
|
||||
@ -86,6 +90,10 @@ def test_get_param_value_with_generic_provider()
|
||||
self.produce_value_called += 1
|
||||
return self.value
|
||||
end
|
||||
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
var tracking_provider = TrackingValueProvider(mock_engine, 42)
|
||||
@ -136,6 +144,10 @@ def test_get_param_value_with_context_aware_provider()
|
||||
return self.base_value
|
||||
end
|
||||
end
|
||||
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
var context_provider = ContextAwareProvider(mock_engine, 5)
|
||||
|
||||
@ -7,7 +7,7 @@ print("Testing nillable parameter attribute...")
|
||||
|
||||
# Create a test class with nillable and non-nillable parameters
|
||||
class TestParameterizedClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"nillable_param": {"type": "int", "nillable": true},
|
||||
"non_nillable_param": {"type": "int"} # No default, no nillable
|
||||
})
|
||||
|
||||
@ -129,7 +129,7 @@ def test_type_validation()
|
||||
|
||||
# Create a test class with different parameter types
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"int_param": {"default": 42}, # Default type is "int"
|
||||
"explicit_int_param": {"type": "int", "default": 10},
|
||||
"string_param": {"type": "string", "default": "hello"},
|
||||
@ -141,6 +141,9 @@ def test_type_validation()
|
||||
def init(engine)
|
||||
super(self).init(engine)
|
||||
end
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
var test_obj = TestClass(engine)
|
||||
|
||||
@ -26,7 +26,7 @@ def test_parameterized_object_basic()
|
||||
class TestObject : animation.parameterized_object
|
||||
# No instance variables for parameters - they're handled by the virtual system
|
||||
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"test_value": {"min": 0, "max": 100, "default": 50},
|
||||
"test_name": {"type": "string", "default": "test"},
|
||||
"test_enum": {"enum": [1, 2, 3], "default": 1}
|
||||
@ -95,7 +95,7 @@ def test_parameter_hierarchy()
|
||||
|
||||
# Create a base class with some parameters
|
||||
class BaseClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"base_param": {"type": "string", "default": "base_value"},
|
||||
"shared_param": {"type": "string", "default": "base_default"}
|
||||
})
|
||||
@ -107,7 +107,7 @@ def test_parameter_hierarchy()
|
||||
|
||||
# Create a child class with additional parameters
|
||||
class ChildClass : BaseClass
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"child_param": {"min": 0, "max": 10, "default": 5},
|
||||
"shared_param": {"type": "string", "default": "child_default"} # Override parent default
|
||||
})
|
||||
@ -144,7 +144,7 @@ def test_value_provider_as_parameter()
|
||||
|
||||
# Create a simple test class
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"dynamic_value": {"min": 0, "max": 100, "default": 50}
|
||||
})
|
||||
|
||||
@ -165,6 +165,9 @@ def test_value_provider_as_parameter()
|
||||
def produce_value(name, time_ms)
|
||||
return self.test_value
|
||||
end
|
||||
def tostring()
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
var provider = MockValueProvider(mock_engine, 75)
|
||||
@ -193,7 +196,7 @@ def test_parameter_metadata()
|
||||
print("Testing parameter metadata...")
|
||||
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"range_param": {"min": 0, "max": 100, "default": 50},
|
||||
"enum_param": {"enum": [1, 2, 3], "default": 1},
|
||||
"simple_param": {"type": "string", "default": "test"}
|
||||
@ -228,7 +231,7 @@ def test_virtual_member_errors()
|
||||
print("Testing virtual member error handling...")
|
||||
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"valid_param": {"min": 0, "max": 100, "default": 50}
|
||||
})
|
||||
|
||||
@ -279,7 +282,7 @@ def test_undefined_parameter_behavior()
|
||||
import string # Import once at the top of the function
|
||||
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"defined_param": {"min": 0, "max": 100, "default": 50}
|
||||
})
|
||||
|
||||
@ -377,7 +380,7 @@ def test_engine_requirement()
|
||||
print("Testing engine parameter requirement...")
|
||||
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"test_param": {"default": 42}
|
||||
})
|
||||
end
|
||||
@ -403,7 +406,7 @@ def test_equality_operator()
|
||||
print("Testing equality operator...")
|
||||
|
||||
class TestClass : animation.parameterized_object
|
||||
static var PARAMS = encode_constraints({
|
||||
static var PARAMS = animation.enc_params({
|
||||
"test_param": {"default": 42}
|
||||
})
|
||||
|
||||
|
||||
@ -14,16 +14,16 @@ def test_multiple_sequence_managers()
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create multiple sequence managers
|
||||
var seq_manager1 = animation.SequenceManager(engine)
|
||||
var seq_manager2 = animation.SequenceManager(engine)
|
||||
var seq_manager3 = animation.SequenceManager(engine)
|
||||
var seq_manager1 = animation.sequence_manager(engine)
|
||||
var seq_manager2 = animation.sequence_manager(engine)
|
||||
var seq_manager3 = animation.sequence_manager(engine)
|
||||
|
||||
# Register all sequence managers with engine
|
||||
engine.add(seq_manager1)
|
||||
engine.add(seq_manager2)
|
||||
engine.add(seq_manager3)
|
||||
|
||||
assert(engine.sequence_managers.size() == 3, "Engine should have 3 sequence managers")
|
||||
assert(size(engine.root_animation.sequences) == 3, "Engine should have 3 sequence managers")
|
||||
|
||||
# Create test animations using new parameterized API
|
||||
var red_provider = animation.static_color(engine)
|
||||
@ -90,8 +90,8 @@ def test_sequence_manager_coordination()
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create two sequence managers with overlapping timing
|
||||
var seq_manager1 = animation.SequenceManager(engine)
|
||||
var seq_manager2 = animation.SequenceManager(engine)
|
||||
var seq_manager1 = animation.sequence_manager(engine)
|
||||
var seq_manager2 = animation.sequence_manager(engine)
|
||||
|
||||
engine.add(seq_manager1)
|
||||
engine.add(seq_manager2)
|
||||
@ -157,8 +157,8 @@ def test_sequence_manager_engine_integration()
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create sequence managers
|
||||
var seq_manager1 = animation.SequenceManager(engine)
|
||||
var seq_manager2 = animation.SequenceManager(engine)
|
||||
var seq_manager1 = animation.sequence_manager(engine)
|
||||
var seq_manager2 = animation.sequence_manager(engine)
|
||||
|
||||
engine.add(seq_manager1)
|
||||
engine.add(seq_manager2)
|
||||
@ -217,24 +217,24 @@ def test_sequence_manager_removal()
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create sequence managers
|
||||
var seq_manager1 = animation.SequenceManager(engine)
|
||||
var seq_manager2 = animation.SequenceManager(engine)
|
||||
var seq_manager3 = animation.SequenceManager(engine)
|
||||
var seq_manager1 = animation.sequence_manager(engine)
|
||||
var seq_manager2 = animation.sequence_manager(engine)
|
||||
var seq_manager3 = animation.sequence_manager(engine)
|
||||
|
||||
engine.add(seq_manager1)
|
||||
engine.add(seq_manager2)
|
||||
engine.add(seq_manager3)
|
||||
|
||||
assert(engine.sequence_managers.size() == 3, "Should have 3 sequence managers")
|
||||
assert(size(engine.root_animation.sequences) == 3, "Should have 3 sequence managers")
|
||||
|
||||
# Test removing specific sequence manager
|
||||
engine.remove_sequence_manager(seq_manager2)
|
||||
assert(engine.sequence_managers.size() == 2, "Should have 2 sequence managers after removal")
|
||||
engine.remove(seq_manager2)
|
||||
assert(size(engine.root_animation.sequences) == 2, "Should have 2 sequence managers after removal")
|
||||
|
||||
# Verify correct managers remain
|
||||
var found_seq1 = false
|
||||
var found_seq3 = false
|
||||
for seq_mgr : engine.sequence_managers
|
||||
for seq_mgr : engine.root_animation.sequences
|
||||
if seq_mgr == seq_manager1
|
||||
found_seq1 = true
|
||||
elif seq_mgr == seq_manager3
|
||||
@ -245,8 +245,8 @@ def test_sequence_manager_removal()
|
||||
assert(found_seq3 == true, "Sequence manager 3 should remain")
|
||||
|
||||
# Test removing non-existent sequence manager
|
||||
engine.remove_sequence_manager(seq_manager2) # Already removed
|
||||
assert(engine.sequence_managers.size() == 2, "Size should remain 2 after removing non-existent manager")
|
||||
engine.remove(seq_manager2) # Already removed
|
||||
assert(size(engine.root_animation.sequences) == 2, "Size should remain 2 after removing non-existent manager")
|
||||
|
||||
print("✓ Sequence manager removal tests passed")
|
||||
end
|
||||
@ -259,8 +259,8 @@ def test_sequence_manager_clear_all()
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create sequence managers with running sequences
|
||||
var seq_manager1 = animation.SequenceManager(engine)
|
||||
var seq_manager2 = animation.SequenceManager(engine)
|
||||
var seq_manager1 = animation.sequence_manager(engine)
|
||||
var seq_manager2 = animation.sequence_manager(engine)
|
||||
|
||||
engine.add(seq_manager1)
|
||||
engine.add(seq_manager2)
|
||||
@ -304,7 +304,7 @@ def test_sequence_manager_clear_all()
|
||||
|
||||
assert(seq_manager1.is_sequence_running() == false, "Sequence 1 should be stopped after clear")
|
||||
assert(seq_manager2.is_sequence_running() == false, "Sequence 2 should be stopped after clear")
|
||||
assert(engine.sequence_managers.size() == 0, "Should have no sequence managers after clear")
|
||||
assert(size(engine.root_animation.sequences) == 0, "Should have no sequence managers after clear")
|
||||
assert(engine.size() == 0, "Should have no animations after clear")
|
||||
|
||||
print("✓ Clear all tests passed")
|
||||
@ -320,12 +320,12 @@ def test_sequence_manager_stress()
|
||||
# Create many sequence managers
|
||||
var seq_managers = []
|
||||
for i : 0..9 # 10 sequence managers
|
||||
var seq_mgr = animation.SequenceManager(engine)
|
||||
var seq_mgr = animation.sequence_manager(engine)
|
||||
engine.add(seq_mgr)
|
||||
seq_managers.push(seq_mgr)
|
||||
end
|
||||
|
||||
assert(engine.sequence_managers.size() == 10, "Should have 10 sequence managers")
|
||||
assert(size(engine.root_animation.sequences) == 10, "Should have 10 sequence managers")
|
||||
|
||||
# Create sequences for each manager
|
||||
tasmota.set_millis(120000)
|
||||
|
||||
@ -12,14 +12,14 @@ def test_sequence_manager_basic()
|
||||
print("=== SequenceManager Basic Tests ===")
|
||||
|
||||
# Test SequenceManager class exists
|
||||
assert(animation.SequenceManager != nil, "SequenceManager class should be defined")
|
||||
assert(animation.sequence_manager != nil, "SequenceManager class should be defined")
|
||||
|
||||
# Create strip and engine for testing
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Test initialization
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
assert(seq_manager.engine == engine, "Engine should be set correctly")
|
||||
assert(seq_manager.steps != nil, "Steps list should be initialized")
|
||||
assert(seq_manager.steps.size() == 0, "Steps list should be empty initially")
|
||||
@ -45,7 +45,7 @@ def test_sequence_manager_step_creation()
|
||||
test_anim.name = "test"
|
||||
|
||||
# Test fluent interface step creation
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Test push_play_step
|
||||
seq_manager.push_play_step(test_anim, 5000)
|
||||
@ -79,7 +79,7 @@ def test_sequence_manager_execution()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test animations using new parameterized API
|
||||
var color_provider1 = animation.static_color(engine)
|
||||
@ -127,7 +127,7 @@ def test_sequence_manager_timing()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test animation using new parameterized API
|
||||
var color_provider = animation.static_color(engine)
|
||||
@ -181,7 +181,7 @@ def test_sequence_manager_step_info()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test sequence using new parameterized API
|
||||
var color_provider = animation.static_color(engine)
|
||||
@ -211,7 +211,7 @@ def test_sequence_manager_stop()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test sequence using new parameterized API
|
||||
var color_provider = animation.static_color(engine)
|
||||
@ -246,7 +246,7 @@ def test_sequence_manager_is_running()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Test initial state
|
||||
assert(seq_manager.is_sequence_running() == false, "Sequence should not be running initially")
|
||||
@ -284,7 +284,7 @@ def test_sequence_manager_assignment_steps()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create test animation using new parameterized API
|
||||
var color_provider = animation.static_color(engine)
|
||||
@ -343,7 +343,7 @@ def test_sequence_manager_complex_sequence()
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
|
||||
# Create multiple test animations using new parameterized API
|
||||
var red_provider = animation.static_color(engine)
|
||||
@ -425,7 +425,7 @@ def test_sequence_manager_integration()
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Test engine integration
|
||||
var seq_manager = animation.SequenceManager(engine)
|
||||
var seq_manager = animation.sequence_manager(engine)
|
||||
engine.add(seq_manager)
|
||||
|
||||
# Create test sequence using new parameterized API
|
||||
@ -458,7 +458,7 @@ def test_sequence_manager_integration()
|
||||
|
||||
# Test engine cleanup
|
||||
engine.clear()
|
||||
assert(engine.sequence_managers.size() == 0, "Engine should clear sequence managers")
|
||||
assert(size(engine.root_animation.sequences) == 0, "Engine should clear sequence managers")
|
||||
|
||||
print("✓ Integration tests passed")
|
||||
end
|
||||
@ -472,7 +472,7 @@ def test_sequence_manager_parametric_repeat_counts()
|
||||
|
||||
# Test 1: Static repeat count (baseline)
|
||||
var static_repeat_count = 3
|
||||
var seq_manager1 = animation.SequenceManager(engine, static_repeat_count)
|
||||
var seq_manager1 = animation.sequence_manager(engine, static_repeat_count)
|
||||
|
||||
# Test get_resolved_repeat_count with static number
|
||||
var resolved_count = seq_manager1.get_resolved_repeat_count()
|
||||
@ -480,7 +480,7 @@ def test_sequence_manager_parametric_repeat_counts()
|
||||
|
||||
# Test 2: Function-based repeat count (simulating col1.palette_size)
|
||||
var palette_size_function = def (engine) return 5 end # Simulates a palette with 5 colors
|
||||
var seq_manager2 = animation.SequenceManager(engine, palette_size_function)
|
||||
var seq_manager2 = animation.sequence_manager(engine, palette_size_function)
|
||||
|
||||
# Test get_resolved_repeat_count with function
|
||||
resolved_count = seq_manager2.get_resolved_repeat_count()
|
||||
@ -493,7 +493,7 @@ def test_sequence_manager_parametric_repeat_counts()
|
||||
return dynamic_counter <= 1 ? 2 : 4 # First call returns 2, subsequent calls return 4
|
||||
end
|
||||
|
||||
var seq_manager3 = animation.SequenceManager(engine, dynamic_function)
|
||||
var seq_manager3 = animation.sequence_manager(engine, dynamic_function)
|
||||
var first_resolved = seq_manager3.get_resolved_repeat_count()
|
||||
var second_resolved = seq_manager3.get_resolved_repeat_count()
|
||||
assert(first_resolved == 2, f"First dynamic call should return 2, got {first_resolved}")
|
||||
@ -523,7 +523,7 @@ def test_sequence_manager_repeat_execution_with_functions()
|
||||
var repeat_count_func = def (engine) return 3 end
|
||||
|
||||
# Create sequence manager with function-based repeat count
|
||||
var seq_manager = animation.SequenceManager(engine, repeat_count_func)
|
||||
var seq_manager = animation.sequence_manager(engine, repeat_count_func)
|
||||
seq_manager.push_play_step(test_anim, 50) # Short duration for testing
|
||||
|
||||
# Verify repeat count is resolved correctly
|
||||
@ -572,7 +572,7 @@ def test_sequence_manager_palette_size_simulation()
|
||||
# play shutter_animation for duration
|
||||
# col1.next = 1
|
||||
# }
|
||||
var seq_manager = animation.SequenceManager(engine, palette_size_func)
|
||||
var seq_manager = animation.sequence_manager(engine, palette_size_func)
|
||||
seq_manager.push_closure_step(advance_color_func) # Just test the closure execution
|
||||
|
||||
# Test that repeat count is resolved correctly
|
||||
@ -615,7 +615,7 @@ def test_sequence_manager_dynamic_repeat_changes()
|
||||
end
|
||||
|
||||
# Create sequence with dynamic repeat count
|
||||
var seq_manager = animation.SequenceManager(engine, dynamic_repeat_func)
|
||||
var seq_manager = animation.sequence_manager(engine, dynamic_repeat_func)
|
||||
seq_manager.push_play_step(test_anim, 250)
|
||||
|
||||
# Start sequence
|
||||
@ -642,7 +642,7 @@ def test_sequence_manager_dynamic_repeat_changes()
|
||||
return engine.strip != nil ? 3 : 1
|
||||
end
|
||||
|
||||
var seq_manager2 = animation.SequenceManager(engine, engine_dependent_func)
|
||||
var seq_manager2 = animation.sequence_manager(engine, engine_dependent_func)
|
||||
var engine_count = seq_manager2.get_resolved_repeat_count()
|
||||
assert(engine_count == 3, f"Engine-dependent count should be 3, got {engine_count}")
|
||||
|
||||
@ -678,7 +678,7 @@ def test_sequence_manager_complex_parametric_scenario()
|
||||
end
|
||||
|
||||
# Create sequence with parametric repeat
|
||||
var seq_manager = animation.SequenceManager(engine, palette_size_func)
|
||||
var seq_manager = animation.sequence_manager(engine, palette_size_func)
|
||||
seq_manager.push_closure_step(advance_colors_func)
|
||||
|
||||
# Verify sequence setup
|
||||
@ -704,6 +704,239 @@ def test_sequence_manager_complex_parametric_scenario()
|
||||
print("✓ Complex parametric scenario tests passed")
|
||||
end
|
||||
|
||||
def test_sequence_manager_zero_iterations()
|
||||
print("=== SequenceManager Zero Iterations Tests ===")
|
||||
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create test animation
|
||||
var color_provider = animation.static_color(engine)
|
||||
color_provider.color = 0xFFFF0000
|
||||
var test_anim = animation.solid(engine)
|
||||
test_anim.color = color_provider
|
||||
test_anim.priority = 0
|
||||
test_anim.duration = 0
|
||||
test_anim.loop = true
|
||||
test_anim.name = "test"
|
||||
|
||||
# Track execution count
|
||||
var execution_count = 0
|
||||
|
||||
# Test 1: Static zero repeat count
|
||||
var seq_manager = animation.sequence_manager(engine, 0)
|
||||
seq_manager.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager.push_play_step(test_anim, 100)
|
||||
|
||||
# Start the sequence
|
||||
tasmota.set_millis(100000)
|
||||
seq_manager.start(100000)
|
||||
|
||||
# Verify that the sequence did not execute
|
||||
assert(seq_manager.is_running == false, "Sequence with repeat_count=0 should not start")
|
||||
assert(execution_count == 0, f"Sequence with repeat_count=0 should not execute, but executed {execution_count} time(s)")
|
||||
|
||||
# Test 2: Function-based zero repeat count (simulating empty palette)
|
||||
execution_count = 0
|
||||
var zero_func = def (engine) return 0 end
|
||||
var seq_manager2 = animation.sequence_manager(engine, zero_func)
|
||||
seq_manager2.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager2.push_play_step(test_anim, 100)
|
||||
|
||||
# Start the sequence
|
||||
tasmota.set_millis(101000)
|
||||
seq_manager2.start(101000)
|
||||
|
||||
# Verify that the sequence did not execute
|
||||
assert(seq_manager2.is_running == false, "Sequence with function returning 0 should not start")
|
||||
assert(execution_count == 0, f"Sequence with function returning 0 should not execute, but executed {execution_count} time(s)")
|
||||
|
||||
print("✓ Zero iterations tests passed")
|
||||
end
|
||||
|
||||
def test_sequence_manager_zero_palette_size()
|
||||
print("=== SequenceManager Zero Palette Size Tests ===")
|
||||
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create a color cycle with empty palette (palette_size = 0)
|
||||
var col1 = animation.color_cycle(engine)
|
||||
col1.palette = bytes() # Empty palette
|
||||
col1.cycle_period = 0
|
||||
|
||||
# Verify palette size is 0
|
||||
assert(col1.palette_size == 0, f"Empty palette should have size 0, got {col1.palette_size}")
|
||||
|
||||
# Track execution count
|
||||
var execution_count = 0
|
||||
|
||||
# Create sequence with repeat col1.palette_size times (should be 0)
|
||||
var seq_manager = animation.sequence_manager(engine, def (engine) return col1.palette_size end)
|
||||
seq_manager.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager.push_wait_step(100)
|
||||
|
||||
# Start the sequence
|
||||
tasmota.set_millis(102000)
|
||||
seq_manager.start(102000)
|
||||
|
||||
# Verify that the sequence did not execute
|
||||
assert(seq_manager.is_running == false, "Sequence with palette_size=0 should not start")
|
||||
assert(execution_count == 0, f"Sequence with palette_size=0 should not execute, but executed {execution_count} time(s)")
|
||||
|
||||
# Test with non-empty palette for comparison
|
||||
execution_count = 0
|
||||
var col2 = animation.color_cycle(engine)
|
||||
col2.palette = bytes("FFFF0000" "FF00FF00" "FF0000FF") # 3 colors
|
||||
col2.cycle_period = 0
|
||||
|
||||
assert(col2.palette_size == 3, f"Palette with 3 colors should have size 3, got {col2.palette_size}")
|
||||
|
||||
# Create sequence with repeat col2.palette_size times (should be 3)
|
||||
var seq_manager2 = animation.sequence_manager(engine, def (engine) return col2.palette_size end)
|
||||
seq_manager2.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager2.push_wait_step(100)
|
||||
|
||||
# Start the sequence
|
||||
tasmota.set_millis(103000)
|
||||
seq_manager2.start(103000)
|
||||
|
||||
# Verify that the sequence started
|
||||
assert(seq_manager2.is_running == true, "Sequence with palette_size=3 should start")
|
||||
assert(execution_count == 1, f"Sequence with palette_size=3 should execute once initially, got {execution_count}")
|
||||
|
||||
# Simulate multiple updates to complete all iterations
|
||||
var time = 103000
|
||||
while seq_manager2.is_running && execution_count < 10 # Safety limit
|
||||
time += 150
|
||||
seq_manager2.update(time)
|
||||
end
|
||||
|
||||
assert(execution_count == 3, f"Sequence with palette_size=3 should execute 3 times total, got {execution_count}")
|
||||
|
||||
print("✓ Zero palette size tests passed")
|
||||
end
|
||||
|
||||
def test_sequence_manager_boolean_repeat_counts()
|
||||
print("=== SequenceManager Boolean Repeat Count Tests ===")
|
||||
|
||||
# Create strip and engine
|
||||
var strip = global.Leds(30)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Create test animation
|
||||
var color_provider = animation.static_color(engine)
|
||||
color_provider.color = 0xFF00FF00
|
||||
var test_anim = animation.solid(engine)
|
||||
test_anim.color = color_provider
|
||||
test_anim.priority = 0
|
||||
test_anim.duration = 0
|
||||
test_anim.loop = true
|
||||
test_anim.name = "test_bool"
|
||||
|
||||
# Test 1: repeat_count = true (should execute once, true converts to 1)
|
||||
var execution_count = 0
|
||||
var seq_manager1 = animation.sequence_manager(engine, true)
|
||||
seq_manager1.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager1.push_wait_step(100)
|
||||
|
||||
# Verify repeat count resolution
|
||||
var resolved_count = seq_manager1.get_resolved_repeat_count()
|
||||
assert(resolved_count == 1, f"Boolean true should resolve to 1, got {resolved_count}")
|
||||
|
||||
# Start and run sequence
|
||||
tasmota.set_millis(104000)
|
||||
seq_manager1.start(104000)
|
||||
|
||||
var time = 104000
|
||||
while seq_manager1.is_running && execution_count < 10
|
||||
time += 150
|
||||
seq_manager1.update(time)
|
||||
end
|
||||
|
||||
assert(execution_count == 1, f"Sequence with repeat_count=true should execute once, got {execution_count}")
|
||||
|
||||
# Test 2: repeat_count = false (should not execute, false converts to 0)
|
||||
execution_count = 0
|
||||
var seq_manager2 = animation.sequence_manager(engine, false)
|
||||
seq_manager2.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager2.push_wait_step(100)
|
||||
|
||||
# Verify repeat count resolution
|
||||
resolved_count = seq_manager2.get_resolved_repeat_count()
|
||||
assert(resolved_count == 0, f"Boolean false should resolve to 0, got {resolved_count}")
|
||||
|
||||
# Start sequence
|
||||
tasmota.set_millis(105000)
|
||||
seq_manager2.start(105000)
|
||||
|
||||
# Verify that the sequence did not execute
|
||||
assert(seq_manager2.is_running == false, "Sequence with repeat_count=false should not start")
|
||||
assert(execution_count == 0, f"Sequence with repeat_count=false should not execute, got {execution_count}")
|
||||
|
||||
# Test 3: Function returning true
|
||||
execution_count = 0
|
||||
var bool_func_true = def (engine) return true end
|
||||
var seq_manager3 = animation.sequence_manager(engine, bool_func_true)
|
||||
seq_manager3.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager3.push_wait_step(100)
|
||||
|
||||
# Verify repeat count resolution
|
||||
resolved_count = seq_manager3.get_resolved_repeat_count()
|
||||
assert(resolved_count == 1, f"Function returning true should resolve to 1, got {resolved_count}")
|
||||
|
||||
# Start and run sequence
|
||||
tasmota.set_millis(106000)
|
||||
seq_manager3.start(106000)
|
||||
|
||||
time = 106000
|
||||
while seq_manager3.is_running && execution_count < 10
|
||||
time += 150
|
||||
seq_manager3.update(time)
|
||||
end
|
||||
|
||||
assert(execution_count == 1, f"Sequence with function returning true should execute once, got {execution_count}")
|
||||
|
||||
# Test 4: Function returning false
|
||||
execution_count = 0
|
||||
var bool_func_false = def (engine) return false end
|
||||
var seq_manager4 = animation.sequence_manager(engine, bool_func_false)
|
||||
seq_manager4.push_closure_step(def (engine)
|
||||
execution_count += 1
|
||||
end)
|
||||
seq_manager4.push_wait_step(100)
|
||||
|
||||
# Verify repeat count resolution
|
||||
resolved_count = seq_manager4.get_resolved_repeat_count()
|
||||
assert(resolved_count == 0, f"Function returning false should resolve to 0, got {resolved_count}")
|
||||
|
||||
# Start sequence
|
||||
tasmota.set_millis(107000)
|
||||
seq_manager4.start(107000)
|
||||
|
||||
# Verify that the sequence did not execute
|
||||
assert(seq_manager4.is_running == false, "Sequence with function returning false should not start")
|
||||
assert(execution_count == 0, f"Sequence with function returning false should not execute, got {execution_count}")
|
||||
|
||||
print("✓ Boolean repeat count tests passed")
|
||||
end
|
||||
|
||||
# Run all tests
|
||||
def run_all_sequence_manager_tests()
|
||||
print("Starting SequenceManager Unit Tests...")
|
||||
@ -723,6 +956,9 @@ def run_all_sequence_manager_tests()
|
||||
test_sequence_manager_palette_size_simulation()
|
||||
test_sequence_manager_dynamic_repeat_changes()
|
||||
test_sequence_manager_complex_parametric_scenario()
|
||||
test_sequence_manager_zero_iterations()
|
||||
test_sequence_manager_zero_palette_size()
|
||||
test_sequence_manager_boolean_repeat_counts()
|
||||
|
||||
print("\n🎉 All SequenceManager tests passed!")
|
||||
return true
|
||||
@ -747,5 +983,8 @@ return {
|
||||
"test_sequence_manager_repeat_execution_with_functions": test_sequence_manager_repeat_execution_with_functions,
|
||||
"test_sequence_manager_palette_size_simulation": test_sequence_manager_palette_size_simulation,
|
||||
"test_sequence_manager_dynamic_repeat_changes": test_sequence_manager_dynamic_repeat_changes,
|
||||
"test_sequence_manager_complex_parametric_scenario": test_sequence_manager_complex_parametric_scenario
|
||||
"test_sequence_manager_complex_parametric_scenario": test_sequence_manager_complex_parametric_scenario,
|
||||
"test_sequence_manager_zero_iterations": test_sequence_manager_zero_iterations,
|
||||
"test_sequence_manager_zero_palette_size": test_sequence_manager_zero_palette_size,
|
||||
"test_sequence_manager_boolean_repeat_counts": test_sequence_manager_boolean_repeat_counts
|
||||
}
|
||||
@ -159,7 +159,7 @@ def test_complex_symbol_dependencies()
|
||||
assert(string.find(berry_code, "var primary_color_") >= 0, "Should define primary color")
|
||||
assert(string.find(berry_code, "var gradient_pattern_") >= 0, "Should define gradient pattern")
|
||||
assert(string.find(berry_code, "var complex_anim_") >= 0, "Should define complex animation")
|
||||
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence")
|
||||
assert(string.find(berry_code, "var demo_ = animation.sequence_manager(engine)") >= 0, "Should define sequence")
|
||||
|
||||
print("✓ Complex symbol dependencies test passed")
|
||||
return true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user