Berry animation meter (#24193)

* Remove tab from json

* Berry animation meter and other optimizations
This commit is contained in:
s-hadinger 2025-12-06 16:52:43 +01:00 committed by GitHub
parent 5f7cb57ffb
commit 8feff1148a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 11476 additions and 12088 deletions

View File

@ -1,67 +0,0 @@
# 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

View File

@ -1,77 +0,0 @@
# Template animation with flags
template animation shutter_bidir {
param colors type palette
param period default 2s
param ascending type bool default true # define to true to enable 'ascending' part
param descending type bool default true # define to true to enable 'descending' part
# since 'strip_length()' is a value provider, it must be assigned to a variable before being used
set strip_len = strip_length()
# animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
# define two rotating palettes, shifted by one color
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1 # move 'col2' to the next color so it's shifte by one compared to 'col1'
# shutter moving in ascending
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving in descending
animation shutter_rl_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
# this is the overall sequence composed of two sub-sequences
# the first in ascending mode, the second in descending
sequence shutter_seq repeat forever {
if ascending { # conditional execution: run only if 'ascending' is true
repeat col1.palette_size times { # run the shutter animation
restart shutter_size # resync all times for this animation, to avoid temporal drift
play shutter_lr_animation for period # run the animation
col1.next = 1 # then move to next color for both palettes
col2.next = 1
}
}
if descending { # conditional execution: run only if 'descending' is true
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

View File

@ -1,164 +0,0 @@
# 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
-#

View File

@ -1,182 +0,0 @@
# Generated Berry code from Animation DSL
# Source: chap_5_22_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 with flags
# Template animation class: shutter_bidir
class shutter_bidir_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"colors": {"type": "palette"},
"period": {"default": 2000},
"ascending": {"type": "bool", "default": true},
"descending": {"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)
# animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
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)
# define two rotating palettes, shifted by one color
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 # move 'col2' to the next color so it's shifte by one compared to 'col1'
# shutter moving in ascending
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 in descending
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
# this is the overall sequence composed of two sub-sequences
# the first in ascending mode, the second in descending
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return bool(self.ascending) end)
# conditional execution: run only if 'ascending' is true
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
# run the shutter animation
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end) # resync all times for this animation, to avoid temporal drift
.push_play_step(shutter_lr_animation_, def (engine) return self.period end) # run the animation
.push_closure_step(def (engine) col1_.next = 1 end) # then move to next color for both palettes
.push_closure_step(def (engine) col2_.next = 1 end)
)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return bool(self.descending) end)
# conditional execution: run only if 'descending' is true
.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
)
var main_ = shutter_bidir_animation(engine)
main_.colors = rainbow_with_white_
main_.period = 1500
engine.add(main_)
engine.run()
#- Original DSL source:
# Template animation with flags
template animation shutter_bidir {
param colors type palette
param period default 2s
param ascending type bool default true # define to true to enable 'ascending' part
param descending type bool default true # define to true to enable 'descending' part
# since 'strip_length()' is a value provider, it must be assigned to a variable before being used
set strip_len = strip_length()
# animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
# define two rotating palettes, shifted by one color
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1 # move 'col2' to the next color so it's shifte by one compared to 'col1'
# shutter moving in ascending
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving in descending
animation shutter_rl_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
# this is the overall sequence composed of two sub-sequences
# the first in ascending mode, the second in descending
sequence shutter_seq repeat forever {
if ascending { # conditional execution: run only if 'ascending' is true
repeat col1.palette_size times { # run the shutter animation
restart shutter_size # resync all times for this animation, to avoid temporal drift
play shutter_lr_animation for period # run the animation
col1.next = 1 # then move to next color for both palettes
col2.next = 1
}
}
if descending { # conditional execution: run only if 'descending' is true
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
-#

View File

@ -61,42 +61,6 @@ 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
```
## chap_5_22_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
@ -448,7 +412,7 @@ stack traceback:
### Compilation Output
```
dsl_compilation_error: Line 4: Transpilation failed: Line 4: Cannot redefine built-in symbol 'abs' (type: 4). Use a different name like 'abs_custom' or 'my_abs'
dsl_compilation_error: Line 4: Transpilation failed: Line 4: Cannot redefine built-in symbol 'abs'. Use a different name like 'abs_custom' or 'my_abs'
stack traceback:
<unknown source>: in function `error`
<unknown source>: in function `transpile`
@ -1256,16 +1220,14 @@ SUCCESS
## Summary
- **Total files processed:** 51
- **Successfully compiled:** 48
- **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
- ✅ chap_5_22_template_shutter_bidir.anim
- ✅ christmas_tree.anim
- ✅ comet_chase.anim
- ✅ computed_values_demo.anim

View File

@ -35,7 +35,7 @@ var rainbow_with_white_ = bytes(
)
# define a gradient across the whole strip
var back_pattern_ = animation.palette_meter_animation(engine)
back_pattern_.value_func = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_meter')(engine) end)
back_pattern_.level = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_meter')(engine) end)
engine.add(back_pattern_)
engine.run()
@ -67,7 +67,7 @@ palette rainbow_with_white = [
]
# define a gradient across the whole strip
animation back_pattern = palette_meter_animation(value_func = rand_meter)
animation back_pattern = palette_meter_animation(level = rand_meter())
run back_pattern

View File

@ -24,6 +24,6 @@ palette rainbow_with_white = [
]
# define a gradient across the whole strip
animation back_pattern = palette_meter_animation(value_func = rand_meter)
animation back_pattern = palette_meter_animation(level = rand_meter())
run back_pattern

View File

@ -1,32 +1,21 @@
# 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
"""
extern function rand_meter
# Vue-meter based on random data
# 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
palette vue_meter_palette = [
( 0, 0x00FF00) # Green
(143, 0x00FF00)
(164, 0xFFFF00) # Yellow
(207, 0xFFFF00)
(228, 0xFF0000) # Red
(255, 0xFF0000)
]
# define a color attribute cycles color in space
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
# define a color palette pattern for 'vue_meter_palette'
color rainbow_rich_color = rich_palette(palette=vue_meter_palette, cycle_period=0, transition_type=LINEAR)
# define a gradient across the whole strip
animation back_pattern = palette_meter_animation(color_source = rainbow_rich_color, value_func = rand_meter)
# define a vue-meter based on all elements above
animation back_pattern = palette_meter_animation(color_source = rainbow_rich_color, level = 85%)
run back_pattern

View File

@ -0,0 +1,30 @@
# Vue-meter based on random data
berry """
# define a pseudo-random generator, returns value in range 0..255
def rand_meter(engine)
return (engine.time_ms * 2654435761) & 0xFF
end
"""
extern function rand_meter # declare the external function
# define a palette of rainbow colors including white with constant brightness
palette vue_meter_palette = [
( 0, 0x00FF00) # Green
(143, 0x00FF00)
(164, 0xFFFF00) # Yellow
(207, 0xFFFF00)
(228, 0xFF0000) # Red
(255, 0xFF0000)
]
# define a color palette pattern for 'vue_meter_palette'
color rainbow_rich_color = rich_palette(palette=vue_meter_palette, cycle_period=0, transition_type=LINEAR)
# define a vue-meter based on all elements above
animation back_pattern = palette_meter_animation(color_source = rainbow_rich_color, level = rand_meter())
run back_pattern

View File

@ -0,0 +1,17 @@
# Sky
# Dark blue background
color space_blue = 0x000066 # Note: opaque 0xFF alpha channel is implicitly added
animation background = solid(color=space_blue)
# Add sparkle trail behind comets but on top of blue background
animation stars = twinkle_animation(
color=0xFFFFAA # Light blue sparkles
density=8 # density (moderate sparkles)
twinkle_speed=400ms # twinkle speed (quick sparkle)
priority = 8
)
# Start all animations
run background
run stars

View File

@ -23,7 +23,8 @@ ParameterizedObject (base class with parameter management and playable interface
│ ├── CrenelPositionAnimation (crenel/square wave pattern)
│ ├── BreatheAnimation (breathing effect)
│ ├── PaletteGradientAnimation (gradient patterns with palette colors)
│ │ └── PaletteMeterAnimation (meter/bar patterns)
│ │ ├── PaletteMeterAnimation (meter/bar patterns)
│ │ └── GradientMeterAnimation (VU meter with gradient colors and peak hold)
│ ├── CometAnimation (moving comet with tail)
│ ├── FireAnimation (realistic fire effect)
│ ├── TwinkleAnimation (twinkling stars effect)
@ -65,7 +66,7 @@ This unified base class enables:
**Key Methods**:
- `start(time_ms)` - Start the object at a specific time
- `stop()` - Stop the object
- `update(time_ms)` - Update object state based on current time
- `update(time_ms)` - Update object state based on current time (no return value)
**Factory**: N/A (base class)
@ -75,7 +76,7 @@ Unified base class for all visual elements. Inherits from `ParameterizedObject`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `name` | string | "animation" | - | Optional name for the animation |
| `id` | string | "animation" | - | Optional name for the animation |
| `is_running` | bool | false | - | Whether the animation is active |
| `priority` | int | 10 | 0-255 | Rendering priority (higher = on top) |
| `duration` | int | 0 | min: 0 | Animation duration in ms (0 = infinite) |
@ -158,7 +159,7 @@ Template animation parameters support all standard constraints:
**Implicit Parameters**:
Template animations automatically inherit parameters from the `EngineProxy` class hierarchy without explicit declaration:
- `name` (string, default: "animation") - Animation name
- `id` (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
@ -213,6 +214,8 @@ Base interface for all value providers. Inherits from `ParameterizedObject`.
**Timing Behavior**: For value providers, `start()` is typically not called because instances can be embedded in closures. Value providers consider the first call to `produce_value()` as the start of their internal time reference. The `start()` method only resets the time origin if the provider was already started previously (i.e., `self.start_time` is not nil).
**Update Method**: The `update(time_ms)` method does not return any value. Subclasses should check `self.is_running` to determine if the object is still active.
**Factory**: N/A (base interface)
### StaticValueProvider
@ -611,7 +614,48 @@ Creates smooth color gradients that can be linear or radial. Inherits from `Anim
**Factories**: `animation.gradient_animation(engine)`, `animation.gradient_rainbow_linear(engine)`, `animation.gradient_rainbow_radial(engine)`, `animation.gradient_two_color_linear(engine)`
### GradientMeterAnimation
VU meter style animation that displays a gradient-colored bar from the start of the strip up to a configurable level. Includes optional peak hold indicator. Inherits from `PaletteGradientAnimation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `level` | int | 255 | 0-255 | Current meter level (0=empty, 255=full) |
| `peak_hold` | int | 1000 | min: 0 | Peak hold time in ms (0=disabled) |
| *(inherits all PaletteGradientAnimation parameters)* | | | | |
#### Visual Representation
```
level=128 (50%), peak at 200
[████████████████--------•-------]
^ ^
| peak indicator (single pixel)
filled gradient area
```
#### Usage Examples
```berry
# Simple meter with rainbow gradient
color rainbow = rich_palette()
animation meter = gradient_meter_animation()
meter.color_source = rainbow
meter.level = 128
# Meter with peak hold (1 second)
color fire_colors = rich_palette(palette=PALETTE_FIRE)
animation vu_meter = gradient_meter_animation(peak_hold=1000)
vu_meter.color_source = fire_colors
# Dynamic level from value provider
set audio_level = triangle(min_value=0, max_value=255, period=2s)
animation audio_meter = gradient_meter_animation(peak_hold=500)
audio_meter.color_source = rainbow
audio_meter.level = audio_level
```
**Factory**: `animation.gradient_meter_animation(engine)`
### NoiseAnimation

View File

@ -38,18 +38,22 @@ class MyAnimation : animation.animation
# Parameter validation is handled automatically by the framework
end
def render(frame, time_ms)
# Update animation state (no return value needed)
def update(time_ms)
super(self).update(time_ms)
# Your update logic here
end
def render(frame, time_ms, strip_length)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Use virtual parameter access - automatically resolves ValueProviders
var param1 = self.my_param1
var param2 = self.my_param2
# Use strip_length parameter instead of self.engine.strip_length for performance
# Your rendering logic here
# ...
@ -153,20 +157,16 @@ end
The virtual parameter system automatically resolves ValueProviders when you access parameters:
```berry
def render(frame, time_ms)
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
def render(frame, time_ms, strip_length)
# Virtual parameter access automatically resolves ValueProviders
var color = self.color # Returns current color value, not the provider
var position = self.pos # Returns current position value
var size = self.size # Returns current size value
# Use strip_length parameter (computed once by engine_proxy) instead of self.engine.strip_length
# Use resolved values in rendering logic
for i: position..(position + size - 1)
if i >= 0 && i < frame.width
if i >= 0 && i < strip_length
frame.set_pixel_color(i, color)
end
end
@ -198,7 +198,7 @@ anim.pos = animation.triangle(0, 29, 3000)
For performance-critical code, cache parameter values:
```berry
def render(frame, time_ms)
def render(frame, time_ms, strip_length)
# Cache parameter values to avoid multiple virtual member access
var current_color = self.color
var current_pos = self.pos
@ -206,7 +206,7 @@ def render(frame, time_ms)
# Use cached values in loops
for i: current_pos..(current_pos + current_size - 1)
if i >= 0 && i < frame.width
if i >= 0 && i < strip_length
frame.set_pixel_color(i, current_color)
end
end
@ -389,24 +389,17 @@ end
### Frame Buffer Operations
```berry
def render(frame, time_ms)
def render(frame, time_ms, strip_length)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Get frame dimensions
var width = frame.width
var height = frame.height # Usually 1 for LED strips
# Resolve dynamic parameters
var color = self.resolve_value(self.color, "color", time_ms)
var opacity = self.resolve_value(self.opacity, "opacity", time_ms)
# Render your effect
for i: 0..(width-1)
# Render your effect using strip_length parameter
for i: 0..(strip_length-1)
var pixel_color = calculate_pixel_color(i, time_ms)
frame.set_pixel_color(i, pixel_color)
end
@ -487,19 +480,12 @@ class BeaconAnimation : animation.animation
end
# Render the pulse to the provided frame buffer
def render(frame, time_ms)
def render(frame, time_ms, strip_length)
if frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
end
var pixel_size = frame.width
var pixel_size = strip_length
# Use virtual parameter access - automatically resolves ValueProviders
var back_color = self.back_color
var pos = self.pos
@ -636,7 +622,7 @@ def test_my_animation()
# Test rendering
var frame = animation.frame_buffer(10)
anim.start()
var result = anim.render(frame, 1000)
var result = anim.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render successfully")
print("✓ All tests passed")

View File

@ -1119,7 +1119,7 @@ Template animations automatically inherit parameters from the `engine_proxy` cla
```berry
# These parameters are implicitly available in all template animations:
param name type string default "animation"
param id type string default "animation"
param priority type int default 10
param duration type int default 0
param loop type bool default false

View File

@ -163,7 +163,7 @@ _add_inherited_params_to_template(template_params_map)
```
**Inherited Parameters (from Animation and ParameterizedObject):**
- `name` (string, default: "animation")
- `id` (string, default: "animation")
- `priority` (int, default: 10)
- `duration` (int, default: 0)
- `loop` (bool, default: false)

View File

@ -153,6 +153,8 @@ import "animations/twinkle" as twinkle_animation
register_to_animation(twinkle_animation)
import "animations/gradient" as gradient_animation
register_to_animation(gradient_animation)
import "animations/palette_meter" as palette_meter_animation
register_to_animation(palette_meter_animation)
import "animations/noise" as noise_animation
register_to_animation(noise_animation)
# import "animations/plasma" as plasma_animation

View File

@ -34,13 +34,10 @@ class BeaconAnimation : animation.animation
# Render the beacon to the provided frame buffer
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Optional current time in milliseconds (defaults to engine time)
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var pixel_size = frame.width
def render(frame, time_ms, strip_length)
# Use virtual parameter access - automatically resolves ValueProviders
var back_color = self.back_color
var pos = self.pos
@ -61,8 +58,8 @@ class BeaconAnimation : animation.animation
if beacon_min < 0
beacon_min = 0
end
if beacon_max >= pixel_size
beacon_max = pixel_size
if beacon_max >= strip_length
beacon_max = strip_length
end
# Draw the main beacon
@ -83,8 +80,8 @@ class BeaconAnimation : animation.animation
if left_slew_min < 0
left_slew_min = 0
end
if left_slew_max >= pixel_size
left_slew_max = pixel_size
if left_slew_max >= strip_length
left_slew_max = strip_length
end
i = left_slew_min
@ -103,8 +100,8 @@ class BeaconAnimation : animation.animation
if right_slew_min < 0
right_slew_min = 0
end
if right_slew_max >= pixel_size
right_slew_max = pixel_size
if right_slew_max >= strip_length
right_slew_max = strip_length
end
i = right_slew_min

View File

@ -53,16 +53,7 @@ class CometAnimation : animation.animation
# Update animation state based on current time
#
# @param time_ms: int - current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# Call parent update method first
if !super(self).update(time_ms)
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Cache parameter values for performance (read once, use multiple times)
var current_speed = self.speed
var current_direction = self.direction
@ -106,20 +97,15 @@ class CometAnimation : animation.animation
self.direction = -current_direction
end
end
return true
end
# Render the comet to the provided frame buffer
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
def render(frame, time_ms, strip_length)
# Get the integer position of the head (convert from 1/256th pixels to pixels)
var head_pixel = self.head_position / 256
@ -129,7 +115,6 @@ class CometAnimation : animation.animation
var direction = self.direction
var wrap_around = self.wrap_around
var fade_factor = self.fade_factor
var strip_length = self.engine.strip_length
# Extract color components from current color (ARGB format)
var head_a = (current_color >> 24) & 0xFF

View File

@ -37,18 +37,10 @@ class CrenelPositionAnimation : animation.animation
# Render the crenel pattern to the provided frame buffer
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Optional current time in milliseconds (defaults to self.engine.time_ms)
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var pixel_size = frame.width
def render(frame, time_ms, strip_length)
# Access parameters via virtual members (automatically resolves ValueProviders)
var back_color = self.back_color
var pos = self.pos
@ -87,7 +79,7 @@ class CrenelPositionAnimation : animation.animation
end
# Render pulses
while (pos < pixel_size) && (nb_pulse != 0)
while (pos < strip_length) && (nb_pulse != 0)
var i = 0
if pos < 0
i = -pos
@ -95,7 +87,7 @@ class CrenelPositionAnimation : animation.animation
# Invariant: pos + i >= 0
# Draw the pulse pixels
while (i < pulse_size) && (pos + i < pixel_size)
while (i < pulse_size) && (pos + i < strip_length)
frame.set_pixel_color(pos + i, color)
i += 1
end

View File

@ -77,16 +77,7 @@ class FireAnimation : animation.animation
# Update animation state based on current time
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# Call parent update method first
if !super(self).update(time_ms)
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Check if it's time to update the fire simulation
# Update frequency is based on flicker_speed (Hz)
var flicker_speed = self.flicker_speed # Cache parameter value
@ -95,8 +86,6 @@ class FireAnimation : animation.animation
self.last_update = time_ms
self._update_fire_simulation(time_ms)
end
return true
end
# Update the fire simulation
@ -231,18 +220,10 @@ class FireAnimation : animation.animation
# Render the fire to the provided frame buffer
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Optional current time in milliseconds (defaults to engine time)
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
# Render each pixel with its current color
var i = 0
while i < strip_length

View File

@ -64,9 +64,7 @@ class GradientAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Cache parameter values for performance
var movement_speed = self.movement_speed
@ -83,8 +81,6 @@ class GradientAnimation : animation.animation
# Calculate gradient colors
self._calculate_gradient(time_ms)
return true
end
# Calculate gradient colors for all pixels
@ -197,15 +193,7 @@ class GradientAnimation : animation.animation
end
# Render gradient to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < strip_length && i < frame.width
if i < size(self.current_colors)

View File

@ -182,9 +182,7 @@ class NoiseAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Update time offset based on speed
var current_speed = self.speed
@ -199,8 +197,6 @@ class NoiseAnimation : animation.animation
# Calculate noise colors
self._calculate_noise(time_ms)
return true
end
# Calculate noise colors for all pixels
@ -230,15 +226,7 @@ class NoiseAnimation : animation.animation
end
# Render noise to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < strip_length
if i < frame.width

View File

@ -0,0 +1,148 @@
# GradientMeterAnimation - VU meter style animation with palette gradient colors
#
# Displays a gradient-colored bar from the start of the strip up to a level (0-255).
# Includes optional peak hold indicator that shows the maximum level for a configurable time.
#
# Visual representation:
# level=128 (50%), peak at 200
# [████████████████--------•-------]
# ^ ^
# | peak indicator (single pixel)
# filled gradient area
import "./core/param_encoder" as encode_constraints
#@ solidify:GradientMeterAnimation,weak
class GradientMeterAnimation : animation.palette_gradient_animation
# Instance variables for peak tracking
var peak_level # Current peak level (0-255)
var peak_time # Time when peak was set (ms)
var _level # Cached value for 'self.level'
# Parameter definitions - extends PaletteGradientAnimation params
static var PARAMS = animation.enc_params({
# Inherited from PaletteGradientAnimation: color_source, shift_period, spatial_period, phase_shift
# New meter-specific parameters
"level": {"min": 0, "max": 255, "default": 255},
"peak_hold": {"min": 0, "default": 1000} # 0 = disabled, >0 = hold time in ms
})
# Initialize a new GradientMeterAnimation
def init(engine)
super(self).init(engine)
# Initialize peak tracking
self.peak_level = 0
self.peak_time = 0
self._level = 0
# Override gradient defaults for meter use - static gradient
self.shift_period = 0
end
# Override update to handle peak tracking with absolute time
def update(time_ms)
var peak_hold = self.peak_hold
if peak_hold > 0
var level = self.level
self._level = level # cache value to be used in 'render()'
var peak_level = self.peak_level
# Update peak tracking using absolute time
if level >= peak_level
# New peak detected, or rearm current peak
self.peak_level = level
self.peak_time = time_ms
elif peak_level > 0
# Check if peak hold has expired
var elapsed_since_peak = time_ms - self.peak_time
if elapsed_since_peak > peak_hold
# Peak hold expired, reset to current level
self.peak_level = level
self.peak_time = time_ms
end
end
end
# Call parent update (computes value_buffer with gradient values)
super(self).update(time_ms)
end
# Override render to only display filled pixels and peak indicator
def render(frame, time_ms, strip_length)
var color_source = self.get_param('color_source')
if color_source == nil
return false
end
var elapsed = time_ms - self.start_time
var level = self._level # use cached value in 'update()'
var peak_hold = self.peak_hold
# Calculate fill position (how many pixels to fill)
var fill_pixels = tasmota.scale_uint(level, 0, 255, 0, strip_length)
# Calculate peak pixel position
var peak_pixel = -1
if peak_hold > 0 && self.peak_level > level
peak_pixel = tasmota.scale_uint(self.peak_level, 0, 255, 0, strip_length) - 1
end
# Optimization for LUT patterns
var lut
if isinstance(color_source, animation.color_provider) && (lut := color_source.get_lut()) != nil
var lut_factor = color_source.LUT_FACTOR # default = 1, we have only 128 cached values
var lut_max = 256 >> lut_factor
var i = 0
var frame_ptr = frame.pixels._buffer()
var lut_ptr = lut._buffer()
var buffer = self.value_buffer._buffer()
while (i < fill_pixels)
var byte_value = buffer[i]
var lut_index = byte_value >> lut_factor # Divide by 2 using bit shift
if byte_value == 255
lut_index = lut_max
end
var lut_color_ptr = lut_ptr + (lut_index << 2) # calculate the pointer for LUT color
frame_ptr[0] = lut_color_ptr[0]
frame_ptr[1] = lut_color_ptr[1]
frame_ptr[2] = lut_color_ptr[2]
frame_ptr[3] = lut_color_ptr[3]
# advance to next
i += 1
frame_ptr += 4
end
else
# Render only filled pixels and peak indicator (leave rest transparent)
var i = 0
while i < fill_pixels
var byte_value = self.value_buffer[i]
var color = color_source.get_color_for_value(byte_value, elapsed)
frame.set_pixel_color(i, color)
# Unfilled pixels stay transparent (not rendered)
i += 1
end
end
# Do we need to show peak pixel?
if peak_pixel >= fill_pixels
var byte_value = self.value_buffer[peak_pixel]
var color = color_source.get_color_for_value(byte_value, elapsed)
frame.set_pixel_color(peak_pixel, color)
end
return true
end
# String representation
def tostring()
var level = self.level
var peak_hold = self.peak_hold
return f"GradientMeterAnimation(level={level}, peak_hold={peak_hold}ms, peak={self.peak_level})"
end
end
return {'palette_meter_animation': GradientMeterAnimation}

View File

@ -9,6 +9,8 @@ import "./core/param_encoder" as encode_constraints
#@ solidify:PaletteGradientAnimation,weak
class PaletteGradientAnimation : animation.animation
var value_buffer # Buffer to store values for each pixel (bytes object)
var _spatial_period # Cached spatial_period for static pattern optimization
var _phase_shift # Cached phase_shift for static pattern optimization
# Static definitions of parameters with constraints
static var PARAMS = animation.enc_params({
@ -29,9 +31,6 @@ class PaletteGradientAnimation : animation.animation
# Initialize non-parameter instance variables only
self.value_buffer = bytes()
# Set default name
self.name = "palette_gradient"
# Initialize value buffer with default frame width
self._initialize_value_buffer()
end
@ -52,9 +51,23 @@ class PaletteGradientAnimation : animation.animation
# Update the value buffer to generate gradient pattern
def _update_value_buffer(time_ms, strip_length)
# Cache parameter values for performance
var shift_period = self.shift_period
var spatial_period = self.spatial_period
var phase_shift = self.phase_shift
var shift_period = self.member("shift_period")
var spatial_period = self.member("spatial_period")
var phase_shift = self.member("phase_shift")
# Optimization: for static patterns (shift_period == 0), skip recomputation
# if spatial_period, phase_shift, and strip_length haven't changed
if shift_period == 0
if self._spatial_period != nil &&
self._spatial_period == spatial_period &&
self._phase_shift == phase_shift &&
size(self.value_buffer) == strip_length
return # No changes, skip recomputation
end
# Update cached values
self._spatial_period = spatial_period
self._phase_shift = phase_shift
end
# Determine effective spatial period (0 means full strip)
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
@ -97,16 +110,7 @@ class PaletteGradientAnimation : animation.animation
# Update animation state based on current time
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# Call parent update method first
if !super(self).update(time_ms)
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
@ -119,31 +123,21 @@ class PaletteGradientAnimation : animation.animation
# Update the value buffer
self._update_value_buffer(elapsed, strip_length)
return true
end
# Render the pattern to the provided frame buffer
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Optional current time in milliseconds (defaults to engine time)
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
def render(frame, time_ms, strip_length)
# Get current parameter values (cached for performance)
var color_source = self.get_param('color_source') # use get_param to avoid resolving of color_provider
if color_source == nil
return false
end
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
# Apply colors from the color source to each pixel based on its value
var strip_length = self.engine.strip_length
# Optimization for LUT patterns
var lut
if isinstance(color_source, animation.color_provider) && (lut := color_source.get_lut()) != nil
@ -171,6 +165,8 @@ class PaletteGradientAnimation : animation.animation
frame_ptr += 4
end
else # no LUT, do one color at a time
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
var i = 0
while (i < strip_length)
var byte_value = self.value_buffer[i]
@ -202,54 +198,6 @@ class PaletteGradientAnimation : animation.animation
end
end
# Value meter pattern animation - creates meter/bar patterns based on a value function
#@ solidify:PaletteMeterAnimation,weak
class PaletteMeterAnimation : PaletteGradientAnimation
# Static definitions of parameters with constraints
static var PARAMS = animation.enc_params({
# Meter-specific parameters only
"value_func": {"default": nil, "type": "function"}
})
# Initialize a new meter pattern animation
#
# @param engine: AnimationEngine - Required animation engine reference
def init(engine)
# Call parent constructor
super(self).init(engine)
# Set default name
self.name = "palette_meter"
end
# Override _update_value_buffer to generate meter pattern directly
def _update_value_buffer(time_ms, strip_length)
# Cache parameter values for performance
var value_func = self.value_func
if value_func == nil
return
end
# Cache engine reference to avoid dereferencing
var engine = self.engine
# Get the current value
var current_value = value_func(engine, time_ms, self)
# Calculate the meter position using scale_uint for better precision
var meter_position = tasmota.scale_uint(current_value, 0, 255, 0, strip_length)
# Calculate values for each pixel
var i = 0
while i < strip_length
# Return 255 if pixel is within the meter, 0 otherwise
self.value_buffer[i] = i < meter_position ? 255 : 0
i += 1
end
end
end
return {
'palette_gradient_animation': PaletteGradientAnimation,
'palette_meter_animation': PaletteMeterAnimation
'palette_gradient_animation': PaletteGradientAnimation
}

View File

@ -29,9 +29,6 @@ class RichPaletteAnimation : animation.animation
def init(engine)
super(self).init(engine) # Initialize Animation base class
# Set default name (override inherited default)
self.name = "rich_palette"
# Create internal RichPaletteColorProvider instance
self.color_provider = animation.rich_palette(engine)
@ -71,7 +68,7 @@ class RichPaletteAnimation : animation.animation
# String representation
def tostring()
try
return f"RichPaletteAnimation({self.name}, cycle_period={self.cycle_period}, brightness={self.brightness})"
return f"RichPaletteAnimation(cycle_period={self.cycle_period}, brightness={self.brightness})"
except ..
return "RichPaletteAnimation(uninitialized)"
end

View File

@ -10,8 +10,6 @@
def solid(engine)
# Create animation with engine-only constructor
var anim = animation.animation(engine)
anim.name = "solid"
return anim
end

View File

@ -10,7 +10,6 @@ class TwinkleAnimation : animation.animation
# NO instance variables for parameters - they are handled by the virtual parameter system
# Non-parameter instance variables only
var twinkle_states # Array storing twinkle state for each pixel
var current_colors # bytes() buffer storing ARGB colors (4 bytes per pixel)
var last_update # Last update time for timing
var random_seed # Seed for random number generation
@ -33,32 +32,28 @@ class TwinkleAnimation : animation.animation
super(self).init(engine)
# Initialize non-parameter instance variables only
self.twinkle_states = []
self.current_colors = bytes() # Use bytes() buffer for ARGB colors (4 bytes per pixel)
self.last_update = 0
# Initialize random seed using engine time
self.random_seed = self.engine.time_ms % 65536
# Initialize arrays based on strip length from engine
# Initialize buffer based on strip length from engine
self._initialize_arrays()
end
# Initialize arrays based on current strip length
# Initialize buffer based on current strip length
def _initialize_arrays()
var strip_length = self.engine.strip_length
# Resize arrays
self.twinkle_states.resize(strip_length)
# Create new bytes() buffer for colors (4 bytes per pixel: ARGB)
# Alpha channel serves as the active state: alpha=0 means off, alpha>0 means active
self.current_colors.clear()
self.current_colors.resize(strip_length * 4)
# Initialize all pixels to off state
# Initialize all pixels to off state (transparent = alpha 0)
var i = 0
while i < strip_length
self.twinkle_states[i] = 0 # 0 = off, >0 = brightness level
self.current_colors.set(i * 4, 0x00000000, -4) # Transparent (alpha = 0)
i += 1
end
@ -102,16 +97,7 @@ class TwinkleAnimation : animation.animation
# Update animation state based on current time
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# Call parent update method first
if !super(self).update(time_ms)
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access parameters via virtual members
var twinkle_speed = self.twinkle_speed
@ -122,8 +108,6 @@ class TwinkleAnimation : animation.animation
self.last_update = time_ms
self._update_twinkle_simulation(time_ms)
end
return true
end
# Update the twinkle simulation with alpha-based fading
@ -137,8 +121,8 @@ class TwinkleAnimation : animation.animation
var strip_length = self.engine.strip_length
# Ensure arrays are properly sized
if size(self.twinkle_states) != strip_length || self.current_colors.size() != strip_length * 4
# Ensure buffer is properly sized
if self.current_colors.size() != strip_length * 4
self._initialize_arrays()
end
@ -153,7 +137,6 @@ class TwinkleAnimation : animation.animation
var fade_amount = tasmota.scale_uint(fade_speed, 0, 255, 1, 20)
if alpha <= fade_amount
# Star has faded completely - reset to transparent
self.twinkle_states[i] = 0
self.current_colors.set(i * 4, 0x00000000, -4)
else
# Reduce alpha while keeping RGB components unchanged
@ -169,8 +152,11 @@ class TwinkleAnimation : animation.animation
# For each pixel, check if it should twinkle based on density probability
var j = 0
while j < strip_length
# Only consider pixels that are currently off (transparent)
if self.twinkle_states[j] == 0
# Only consider pixels that are currently off (alpha = 0)
var current_color = self.current_colors.get(j * 4, -4)
var alpha = (current_color >> 24) & 0xFF
if alpha == 0
# Use density as probability out of 255
if self._random_range(255) < density
# Create new star at full brightness with random intensity alpha
@ -185,7 +171,6 @@ class TwinkleAnimation : animation.animation
var b = base_color & 0xFF
# Create new star with full-brightness color and variable alpha
self.twinkle_states[j] = 1 # Mark as active (non-zero)
self.current_colors.set(j * 4, (star_alpha << 24) | (r << 16) | (g << 8) | b, -4)
end
end
@ -196,20 +181,12 @@ class TwinkleAnimation : animation.animation
# Render the twinkle to the provided frame buffer
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Optional current time in milliseconds (defaults to self.engine.time_ms)
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.strip_length
# Ensure arrays are properly sized
if size(self.twinkle_states) != strip_length || self.current_colors.size() != strip_length * 4
def render(frame, time_ms, strip_length)
# Ensure buffer is properly sized
if self.current_colors.size() != strip_length * 4
self._initialize_arrays()
end

View File

@ -97,9 +97,7 @@ class WaveAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Update time offset based on wave speed
var current_wave_speed = self.wave_speed
@ -114,8 +112,6 @@ class WaveAnimation : animation.animation
# Calculate wave colors
self._calculate_wave(time_ms)
return true
end
# Calculate wave colors for all pixels
@ -197,15 +193,7 @@ class WaveAnimation : animation.animation
end
# Render wave to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < strip_length
if i < frame.width && i < self.current_colors.size()

View File

@ -102,9 +102,7 @@ class BounceAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Initialize last_update_time on first update
if self.last_update_time == 0
@ -114,7 +112,7 @@ class BounceAnimation : animation.animation
# Calculate time delta
var dt = time_ms - self.last_update_time
if dt <= 0
return true
return
end
self.last_update_time = time_ms
@ -132,8 +130,6 @@ class BounceAnimation : animation.animation
# Calculate bounced colors
self._calculate_bounce()
return true
end
# Update bounce physics
@ -220,14 +216,9 @@ class BounceAnimation : animation.animation
end
# Render bounce to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
var current_strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < current_strip_length
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
end
@ -255,7 +246,6 @@ def bounce_basic(engine)
bounce.bounce_range = 0 # full strip range
bounce.damping = 250
bounce.gravity = 0
bounce.name = "bounce_basic"
return bounce
end
@ -269,7 +259,6 @@ def bounce_gravity(engine)
bounce.bounce_range = 0 # full strip range
bounce.damping = 240
bounce.gravity = 128
bounce.name = "bounce_gravity"
return bounce
end
@ -283,7 +272,6 @@ def bounce_constrained(engine)
bounce.bounce_range = 15 # constrained range
bounce.damping = 250
bounce.gravity = 0
bounce.name = "bounce_constrained"
return bounce
end

View File

@ -89,9 +89,7 @@ class JitterAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Cache parameter values for performance
var jitter_frequency = self.jitter_frequency
@ -116,8 +114,6 @@ class JitterAnimation : animation.animation
# Calculate jittered colors
self._calculate_jitter()
return true
end
# Update jitter offsets
@ -237,17 +233,9 @@ class JitterAnimation : animation.animation
end
# Render jitter to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < current_strip_length
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
end

View File

@ -100,9 +100,7 @@ class PlasmaAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Update time phase based on speed
var current_time_speed = self.time_speed
@ -117,8 +115,6 @@ class PlasmaAnimation : animation.animation
# Calculate plasma colors
self._calculate_plasma(time_ms)
return true
end
# Calculate plasma colors for all pixels
@ -184,15 +180,7 @@ class PlasmaAnimation : animation.animation
end
# Render plasma to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < strip_length
if i < frame.width
@ -230,7 +218,6 @@ def plasma_rainbow(engine)
# Use default rainbow color (nil triggers rainbow in on_param_changed)
anim.color = nil
anim.time_speed = 50
anim.name = "plasma_rainbow"
return anim
end
@ -244,7 +231,6 @@ def plasma_fast(engine)
anim.time_speed = 150
anim.freq_x = 48
anim.freq_y = 35
anim.name = "plasma_fast"
return anim
end

View File

@ -69,9 +69,7 @@ class ScaleAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Cache parameter values for performance
var current_scale_speed = self.scale_speed
@ -98,8 +96,6 @@ class ScaleAnimation : animation.animation
# Calculate scaled colors
self._calculate_scale()
return true
end
# Calculate current scale factor based on mode
@ -235,17 +231,9 @@ class ScaleAnimation : animation.animation
end
# Render scale to frame buffer
def render(frame, time_ms)
if frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < current_strip_length
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
end

View File

@ -56,9 +56,7 @@ class ShiftAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Cache parameter values for performance
var current_shift_speed = self.shift_speed
@ -95,8 +93,6 @@ class ShiftAnimation : animation.animation
# Calculate shifted colors
self._calculate_shift()
return true
end
# Calculate shifted colors for all pixels
@ -152,17 +148,9 @@ class ShiftAnimation : animation.animation
end
# Render shift to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < current_strip_length
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
end

View File

@ -94,21 +94,17 @@ class SparkleAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
super(self).update(time_ms)
# Update at approximately 30 FPS
var update_interval = 33 # ~30 FPS
if time_ms - self.last_update < update_interval
return true
return
end
self.last_update = time_ms
# Update sparkle simulation
self._update_sparkles(time_ms)
return true
end
# Update sparkle states and create new sparkles
@ -198,17 +194,9 @@ class SparkleAnimation : animation.animation
end
# Render sparkles to frame buffer
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.strip_length
def render(frame, time_ms, strip_length)
var i = 0
while i < current_strip_length
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
end
@ -241,7 +229,6 @@ end
def sparkle_white(engine)
var anim = animation.sparkle_animation(engine)
anim.color = 0xFFFFFFFF # white sparkles
anim.name = "sparkle_white"
return anim
end
@ -256,7 +243,6 @@ def sparkle_rainbow(engine)
var anim = animation.sparkle_animation(engine)
anim.color = rainbow_provider
anim.name = "sparkle_rainbow"
return anim
end

View File

@ -18,7 +18,7 @@ class Animation : animation.parameterized_object
# 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
"id": {"type": "string", "default": ""}, # Optional id 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
@ -40,15 +40,7 @@ class Animation : animation.parameterized_object
# This method should be called regularly by the animation engine
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# do nothing if not running
if (!self.is_running) return false end
# auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access parameters via virtual members
var current_duration = self.duration
@ -66,12 +58,9 @@ class Animation : animation.parameterized_object
# Animation completed, make it inactive
# Set directly in values map to avoid triggering on_param_changed
self.is_running = false
return false
end
end
end
return true
end
# Render the animation to the provided frame buffer
@ -79,12 +68,11 @@ class Animation : animation.parameterized_object
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if (!self.is_running) return false end
def render(frame, time_ms, strip_length)
# Access parameters via virtual members (auto-resolves ValueProviders)
var current_color = self.color
var current_color = self.member("color")
# Fill the entire frame with the current color if not transparent
if (current_color != 0x00000000)
@ -98,7 +86,8 @@ class Animation : animation.parameterized_object
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Current time in milliseconds
def post_render(frame, time_ms)
# @param strip_length: int - Length of the LED strip in pixels
def post_render(frame, time_ms, strip_length)
# no need to auto-fix time_ms and start_time
# Handle opacity - can be number, frame buffer, or animation
var current_opacity = self.opacity
@ -109,7 +98,7 @@ class Animation : animation.parameterized_object
frame.apply_opacity(frame.pixels, current_opacity)
else
# Opacity is a frame buffer
self._apply_opacity(frame, current_opacity, time_ms)
self._apply_opacity(frame, current_opacity, time_ms, strip_length)
end
end
@ -118,7 +107,8 @@ class Animation : animation.parameterized_object
# @param frame: FrameBuffer - The frame buffer to apply opacity to
# @param opacity: int|Animation - Opacity value or animation
# @param time_ms: int - Current time in milliseconds
def _apply_opacity(frame, opacity, time_ms)
# @param strip_length: int - Length of the LED strip in pixels
def _apply_opacity(frame, opacity, time_ms, strip_length)
# Check if opacity is an animation instance
if isinstance(opacity, animation.animation)
# Animation mode: render opacity animation to frame buffer and use as mask
@ -139,7 +129,7 @@ class Animation : animation.parameterized_object
# Update and render opacity animation
opacity_animation.update(time_ms)
opacity_animation.render(self.opacity_frame, time_ms)
opacity_animation.render(self.opacity_frame, time_ms, strip_length)
# Use rendered frame buffer as opacity mask
frame.apply_opacity(frame.pixels, self.opacity_frame.pixels)
@ -166,7 +156,7 @@ class Animation : animation.parameterized_object
# String representation of the animation
def tostring()
return f"Animation({self.name}, priority={self.priority}, duration={self.duration}, loop={self.loop}, running={self.is_running})"
return f"Animation(priority={self.priority}, duration={self.duration}, loop={self.loop}, running={self.is_running})"
end
end

View File

@ -74,7 +74,6 @@ class AnimationEngine
# Create root EngineProxy to manage all children
self.root_animation = animation.engine_proxy(self)
self.root_animation.name = "root"
# Initialize state
self.is_running = false
@ -455,11 +454,11 @@ class AnimationEngine
end
# Interrupt specific animation by name
def interrupt_animation(name)
def interrupt_animation(id)
var i = 0
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
if isinstance(child, animation.animation) && child.id == id
child.stop()
self.root_animation.children.remove(i)
return

View File

@ -254,7 +254,14 @@ class EngineProxy : animation.animation
idx += 1
end
# Start all animations SECOND (they use values from providers and sequences)
# Start all value providers SECOND (they provide dynamic values)
idx = 0
while idx < size(self.value_providers)
self.value_providers[idx].start(time_ms)
idx += 1
end
# Start all animations THIRD (they use values from providers and sequences)
idx = 0
while idx < size(self.animations)
self.animations[idx].start(time_ms)
@ -306,24 +313,27 @@ class EngineProxy : animation.animation
# Update the hybrid animation and all its children
#
# @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
self.strip_length = self.engine.strip_length
self.time_ms = time_ms # We have 'self.time' attribute to mimick 'engine' behavior
self.strip_length = self.engine.strip_length # We have 'self.strip_length' attribute to mimick 'engine' behavior
# Update parent animation state
var still_running = super(self).update(time_ms)
if !still_running
return false
end
super(self).update(time_ms)
# Update all value providers FIRST (they may produce values used by sequences and animations)
var idx = 0
var sz = size(self.value_providers)
while idx < sz
self.value_providers[idx].update(time_ms)
var vp = self.value_providers[idx]
if vp.is_running
# Set start time if needed
if vp.start_time == nil
vp.start_time = time_ms
end
# Call actual update
vp.update(time_ms)
end
idx += 1
end
@ -331,7 +341,15 @@ class EngineProxy : animation.animation
idx = 0
sz = size(self.sequences)
while idx < sz
self.sequences[idx].update(time_ms)
var sq = self.sequences[idx]
if sq.is_running
# Set start time if needed
if sq.start_time == nil
sq.start_time = time_ms
end
# Call actual update
sq.update(time_ms)
end
idx += 1
end
@ -339,11 +357,17 @@ class EngineProxy : animation.animation
idx = 0
sz = size(self.animations)
while idx < sz
var child = self.animations[idx].update(time_ms)
var an = self.animations[idx]
if an.is_running
# Set start time if needed
if an.start_time == nil
an.start_time = time_ms
end
# Call actual update
an.update(time_ms)
end
idx += 1
end
return true
end
# Render the hybrid animation
@ -351,12 +375,18 @@ class EngineProxy : animation.animation
#
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels (optional, defaults to self.strip_length)
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
def render(frame, time_ms, strip_length)
if !self.is_running || frame == nil
return false
end
# Use cached strip_length if not provided
if strip_length == nil
strip_length = self.strip_length
end
# # update sequences first
# var i = 0
# while i < size(self.sequences)
@ -367,7 +397,7 @@ class EngineProxy : animation.animation
var modified = false
# We don't call super method for optimization, skipping color computation
# modified = super(self).render(frame, time_ms)
# modified = super(self).render(frame, time_ms, strip_length)
# Render all child animations (but not sequences - they don't render)
var idx = 0
@ -380,11 +410,11 @@ class EngineProxy : animation.animation
self.temp_buffer.clear()
# Render child
var child_rendered = child.render(self.temp_buffer, time_ms)
var child_rendered = child.render(self.temp_buffer, time_ms, strip_length)
if child_rendered
# Apply child's post-processing
child.post_render(self.temp_buffer, time_ms)
child.post_render(self.temp_buffer, time_ms, strip_length)
# Blend child into main frame
frame.blend_pixels(frame.pixels, self.temp_buffer.pixels)
@ -446,7 +476,7 @@ class EngineProxy : animation.animation
# String representation
def tostring()
return f"{classname(self)}({self.name}, animations={size(self.animations)}, sequences={size(self.sequences)}, value_providers={size(self.value_providers)}, running={self.is_running})"
return f"{classname(self)}(animations={size(self.animations)}, sequences={size(self.sequences)}, value_providers={size(self.value_providers)}, running={self.is_running})"
end
end

View File

@ -108,14 +108,16 @@ class ParameterizedObject
# end
# Check if it's a parameter (either set in values or defined in PARAMS)
# Implement a fast-track if the value exists
if self.values.contains(name)
var value = self.values[name]
# main case, the value is numerical and present, so `find()` will get it in one search
var value = self.values.find(name)
if (value != nil) # in not nil, there is a value
if type(value) != "instance"
return value
end
# Apply produce_value() if it' a ValueProvider
return self.resolve_value(value, name, self.engine.time_ms)
elif self.values.contains(name) # second case, nil is the actual value (and not returned because not found)
return nil
else
# Return default if available from class hierarchy
var encoded_constraints = self._get_param_def(name)
@ -375,9 +377,6 @@ class ParameterizedObject
if time_ms == nil
time_ms = self.engine.time_ms
end
# if time_ms == nil
# raise "value_error", "engine.time_ms should not be 'nil'"
# end
if self.start_time == nil
self.start_time = time_ms
end
@ -428,10 +427,8 @@ class ParameterizedObject
# Subclasses must override this to implement their update logic
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if object is still running, false if completed
def update(time_ms)
# Default implementation just returns running state
return self.is_running
# Default implementation does nothing - subclasses override as needed
end
# Method called when a parameter is changed

View File

@ -177,10 +177,14 @@ class SequenceManager : animation.parameterized_object
end
# Update sequence state - called from fast_loop
# Returns true if still running, false if completed
def update(current_time)
if !self.is_running || size(self.steps) == 0
return false
return
end
# Safety check: ensure step_index is valid
if self.step_index >= size(self.steps)
return
end
var current_step = self.steps[self.step_index]
@ -189,7 +193,8 @@ class SequenceManager : animation.parameterized_object
if current_step["type"] == "subsequence"
# Handle sub-sequence (including repeat sequences)
var sub_seq = current_step["sequence_manager"]
if !sub_seq.update(current_time)
sub_seq.update(current_time)
if !sub_seq.is_running
# Sub-sequence finished, advance to next step
self.advance_to_next_step(current_time)
end
@ -221,8 +226,6 @@ class SequenceManager : animation.parameterized_object
self.advance_to_next_step(current_time)
end
end
return self.is_running
end
# Execute the current step

View File

@ -46,16 +46,11 @@ class SimpleDSLTranspiler
self.instance_for_validation = instance_for_validation # nil by default
end
# Check if this expression needs closure wrapping
# Check if this expression needs closure/function wrapping
def needs_closure()
return self.has_dynamic
end
# Check if this expression needs function wrapping
def needs_function()
return self.has_dynamic
end
# String representation for debugging
def tostring()
var instance_str = (self.instance_for_validation != nil) ? f"instance={classname(self.instance_for_validation)}" : "instance=nil"
@ -367,46 +362,6 @@ class SimpleDSLTranspiler
end
end
# Transpile template body (similar to main transpile but without imports/engine start)
def transpile_template_body()
try
# Process all statements in template body 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 templates, process run statements immediately instead of collecting them
if size(self.run_statements) > 0
for run_stmt : self.run_statements
var obj_name = run_stmt["name"]
var comment = run_stmt["comment"]
# In templates, use underscore suffix for local variables
self.add(f"engine.add({obj_name}_){comment}")
end
end
return self.join_output()
except .. as e, msg
self.error(f"Template body transpilation failed: {msg}")
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()
@ -1430,9 +1385,8 @@ class SimpleDSLTranspiler
def process_value(context)
var result = self.process_additive_expression(context, true, false) # true = top-level, false = not raw mode
# Handle closure wrapping for top-level expressions (not in raw mode) only if there is computation needed
# print(f"> process_value {context=} {result.needs_function()=} {result=}")
if (((context == self.CONTEXT_VARIABLE) || (context == self.CONTEXT_PROPERTY)) && result.needs_closure())
|| ((context == self.CONTEXT_REPEAT_COUNT) && result.needs_function())
|| ((context == self.CONTEXT_REPEAT_COUNT) && result.needs_closure())
# Special handling for repeat_count context - always create simple function for property access
if context == self.CONTEXT_REPEAT_COUNT
# print(f">>> CONTEXT_REPEAT_COUNT")
@ -2317,11 +2271,14 @@ class SimpleDSLTranspiler
self.error(f"Cannot redefine predefined color '{name}'. Use a different name like '{name}_custom' or 'my_{name}'")
return false
elif entry.is_builtin
self.error(f"Cannot redefine built-in symbol '{name}' (type: {entry.type}). Use a different name like '{name}_custom' or 'my_{name}'")
self.error(f"Cannot redefine built-in symbol '{name}'. Use a different name like '{name}_custom' or 'my_{name}'")
return false
elif definition_type == "extern function" && entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-#
# Allow duplicate extern function declarations for the same function
return true
else
# User-defined symbol already exists - this is a redefinition error
self.error(f"Symbol '{name}' is already defined as {entry.type}. Cannot redefine as {definition_type}.")
self.error(f"Symbol '{name}' is already defined. Cannot redefine as {definition_type}.")
return false
end
@ -2389,11 +2346,8 @@ class SimpleDSLTranspiler
end
def join_output()
var result = ""
for line : self.output
result += line + "\n"
end
return result
# Use list.concat() for O(n) performance instead of O(n²) string concatenation
return self.output.concat("\n") + "\n"
end
def error(msg)
@ -2729,6 +2683,15 @@ class SimpleDSLTranspiler
var inline_comment = self.collect_inline_comment()
# Check if already declared (duplicate extern function is allowed, skip code generation)
if self.symbol_table.contains(func_name)
var entry = self.symbol_table.get(func_name)
if entry != nil && entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-#
# Already declared as extern function, skip duplicate registration
return
end
end
# Validate function name
self.validate_user_name(func_name, "extern function")

View File

@ -239,7 +239,6 @@ class RichPaletteColorProvider : animation.color_provider
# Subclasses must override this to implement their update logic
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if object is still running, false if completed
def update(time_ms)
# Rebuild LUT if dirty
if self._lut_dirty || self._color_lut == nil
@ -247,9 +246,7 @@ class RichPaletteColorProvider : animation.color_provider
end
# Cache the brightness to an instance variable for this tick
self._brightness = self.brightness
return self.is_running
self._brightness = self.member("brightness")
end
# Produce a color value for any parameter name (optimized version from Animate_palette)

View File

@ -35,16 +35,6 @@ class ValueProvider : animation.parameterized_object
def produce_value(name, time_ms)
return module("undefined") # Default behavior - return undefined
end
# Update object 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 object is still running, false if completed
def update(time_ms)
# Default implementation just returns running state
return self.is_running
end
end
# Add a method to check if an object is a value provider

View File

@ -42,17 +42,14 @@ print("\n--- Test 2: Animation Management ---")
var anim1 = animation.solid(engine) # Red, priority 10
anim1.color = 0xFFFF0000
anim1.priority = 10
anim1.name = "red"
var anim2 = animation.solid(engine) # Green, priority 5
anim2.color = 0xFF00FF00
anim2.priority = 5
anim2.name = "green"
var anim3 = animation.solid(engine) # Blue, priority 15
anim3.color = 0xFF0000FF
anim3.priority = 15
anim3.name = "blue"
assert_test(engine.add(anim1), "Should add first animation")
assert_test(engine.add(anim2), "Should add second animation")
@ -92,7 +89,6 @@ engine.clear()
var test_anim = animation.solid(engine)
test_anim.color = 0xFFFF0000
test_anim.priority = 10
test_anim.name = "test"
engine.add(test_anim)
engine.run()
@ -136,7 +132,6 @@ for i : 0..49
var anim = animation.solid(engine)
anim.color = color
anim.priority = i
anim.name = f"perf_{i}"
engine.add(anim)
end

View File

@ -40,7 +40,6 @@ var base_anim = animation.solid(opacity_engine)
base_anim.color = 0xFFFF0000 # Red
base_anim.opacity = 128 # 50% opacity
base_anim.priority = 10
base_anim.name = "base_red"
opacity_engine.add(base_anim)
opacity_engine.run()
@ -61,17 +60,17 @@ print("\n--- Test 11b: Animation as opacity mask - basic setup ---")
var opacity_mask = animation.solid(opacity_engine)
opacity_mask.color = 0xFF808080 # Gray (128 brightness)
opacity_mask.priority = 5
opacity_mask.name = "opacity_mask"
opacity_mask.id = "opacity_mask"
# Create main animation with animation opacity
var masked_anim = animation.solid(opacity_engine)
masked_anim.color = 0xFF00FF00 # Green
masked_anim.opacity = opacity_mask # Use animation as opacity
masked_anim.priority = 15
masked_anim.name = "masked_green"
masked_anim.id = "masked_green"
assert_test(isinstance(masked_anim.opacity, animation.animation), "Opacity should be an animation instance")
assert_equals(masked_anim.opacity.name, "opacity_mask", "Opacity animation should be correctly assigned")
assert_equals(masked_anim.opacity.id, "opacity_mask", "Opacity animation should be correctly assigned")
# Test 11c: Animation opacity rendering
print("\n--- Test 11c: Animation opacity rendering ---")
@ -100,14 +99,12 @@ print("\n--- Test 11e: Complex opacity animation scenarios ---")
var pulsing_opacity = animation.solid(opacity_engine)
pulsing_opacity.color = 0xFF000000 # Start with black (0 opacity)
pulsing_opacity.priority = 1
pulsing_opacity.name = "pulsing_opacity"
# Create animated color base
var rainbow_base = animation.solid(opacity_engine)
rainbow_base.color = 0xFFFF0000 # Red base
rainbow_base.opacity = pulsing_opacity # Pulsing opacity
rainbow_base.priority = 20
rainbow_base.name = "rainbow_with_pulse"
# Test multiple renders with changing opacity
opacity_engine.clear()
@ -134,14 +131,12 @@ print("\n--- Test 11f: Opacity animation lifecycle management ---")
var auto_start_opacity = animation.solid(opacity_engine)
auto_start_opacity.color = 0xFF808080 # Gray
auto_start_opacity.priority = 1
auto_start_opacity.name = "auto_start_opacity"
auto_start_opacity.is_running = false # Start stopped
var auto_start_main = animation.solid(opacity_engine)
auto_start_main.color = 0xFFFFFF00 # Yellow
auto_start_main.opacity = auto_start_opacity
auto_start_main.priority = 10
auto_start_main.name = "auto_start_main"
# Opacity animation should not be running initially
assert_test(!auto_start_opacity.is_running, "Opacity animation should start stopped")
@ -163,17 +158,14 @@ print("\n--- Test 11g: Nested animation opacity ---")
var base_nested = animation.solid(opacity_engine)
base_nested.color = 0xFF00FFFF # Cyan
base_nested.priority = 30
base_nested.name = "base_nested"
var opacity1 = animation.solid(opacity_engine)
opacity1.color = 0xFF808080 # 50% gray
opacity1.priority = 25
opacity1.name = "opacity1"
var opacity2 = animation.solid(opacity_engine)
opacity2.color = 0xFFC0C0C0 # 75% gray
opacity2.priority = 20
opacity2.name = "opacity2"
# Chain the opacities: base uses opacity1, opacity1 uses opacity2
opacity1.opacity = opacity2
@ -202,12 +194,10 @@ print("\n--- Test 11h: Opacity animation parameter changes ---")
var param_base = animation.solid(opacity_engine)
param_base.color = 0xFFFF00FF # Magenta
param_base.priority = 10
param_base.name = "param_base"
var param_opacity = animation.solid(opacity_engine)
param_opacity.color = 0xFF404040 # Dark gray
param_opacity.priority = 5
param_opacity.name = "param_opacity"
param_base.opacity = param_opacity
@ -244,7 +234,6 @@ print("\n--- Test 11i: Opacity edge cases ---")
var edge_base = animation.solid(opacity_engine)
edge_base.color = 0xFF0080FF # Blue
edge_base.priority = 10
edge_base.name = "edge_base"
# Test full transparency (should still render but with no visible effect)
edge_base.opacity = 0
@ -264,7 +253,6 @@ assert_test(render_result, "Animation with full opacity should render normally")
var transparent_opacity = animation.solid(opacity_engine)
transparent_opacity.color = 0x00000000 # Fully transparent
transparent_opacity.priority = 5
transparent_opacity.name = "transparent_opacity"
edge_base.opacity = transparent_opacity
transparent_opacity.start()
@ -285,12 +273,10 @@ for i : 0..9
var perf_base = animation.solid(opacity_engine)
perf_base.color = 0xFF000000 | ((i * 25) << 16) | ((i * 15) << 8) | (i * 10)
perf_base.priority = 50 + i
perf_base.name = f"perf_base_{i}"
var perf_opacity = animation.solid(opacity_engine)
perf_opacity.color = 0xFF808080 # 50% gray
perf_opacity.priority = 40 + i
perf_opacity.name = f"perf_opacity_{i}"
perf_base.opacity = perf_opacity

View File

@ -24,14 +24,12 @@ anim.priority = 20
anim.duration = 5000
anim.loop = true # Use boolean for loop parameter
anim.opacity = 255
anim.name = "test_animation"
anim.color = 0xFF0000
assert(anim.is_running == false, "Animation should not be running initially")
assert(anim.priority == 20, "Animation priority should be 20")
assert(anim.duration == 5000, "Animation duration should be 5000ms")
assert(anim.loop == true, "Animation should be set to loop")
assert(anim.opacity == 255, "Animation opacity should be 255")
assert(anim.name == "test_animation", "Animation name should be 'test_animation'")
assert(anim.color == 0xFF0000, "Animation color should be red")
# Test default values
@ -40,19 +38,21 @@ assert(default_anim.priority == 10, "Default priority should be 10")
assert(default_anim.duration == 0, "Default duration should be 0 (infinite)")
assert(default_anim.loop == 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 == 0x00000000, "Default color should be transparent")
# Test start method
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
# because engine_proxy normally sets it before dispatching update/render calls
engine.time_ms = 1000
anim.start()
anim.update()
anim.start_time = engine.time_ms # Set start_time manually for direct testing
anim.start(engine.time_ms)
anim.update(engine.time_ms)
assert(anim.is_running == true, "Animation should be running after start")
assert(anim.start_time == 1000, "Animation start time should be 1000")
# Test restart functionality - start() acts as restart
engine.time_ms = 2000
anim.start()
anim.start(engine.time_ms)
assert(anim.is_running == true, "Animation should be running after start")
assert(anim.start_time == 2000, "Animation start time should be 2000")
var first_start_time = anim.start_time
@ -69,23 +69,21 @@ non_loop_anim.priority = 1
non_loop_anim.duration = 1000
non_loop_anim.loop = false
non_loop_anim.opacity = 255
non_loop_anim.name = "non_loop"
non_loop_anim.color = 0xFF0000
engine.time_ms = 2000
non_loop_anim.start_time = engine.time_ms # Set start_time manually for direct testing
non_loop_anim.start(2000)
non_loop_anim.update(2000)
assert(non_loop_anim.is_running == true, "Animation should be running after start")
# Update within duration
engine.time_ms = 2500
var result = non_loop_anim.update(engine.time_ms)
assert(result == true, "Update should return true when animation is still running")
non_loop_anim.update(engine.time_ms)
assert(non_loop_anim.is_running == true, "Animation should still be running")
# Update after duration
engine.time_ms = 3100
result = non_loop_anim.update(engine.time_ms)
assert(result == false, "Update should return false when animation is complete")
non_loop_anim.update(engine.time_ms)
assert(non_loop_anim.is_running == false, "Animation should stop after duration")
# Test update method with looping animation
@ -94,16 +92,15 @@ loop_anim.priority = 1
loop_anim.duration = 1000
loop_anim.loop = true
loop_anim.opacity = 255
loop_anim.name = "loop"
loop_anim.color = 0xFF0000
engine.time_ms = 4000
loop_anim.start_time = engine.time_ms # Set start_time manually for direct testing
loop_anim.start(engine.time_ms)
loop_anim.update(engine.time_ms) # update must be explictly called to start time
# Update after one loop
engine.time_ms = 5100
result = loop_anim.update(engine.time_ms)
assert(result == true, "Update should return true for looping animation")
loop_anim.update(engine.time_ms)
assert(loop_anim.is_running == true, "Looping animation should still be running after duration")
assert(loop_anim.start_time == 5000, "Start time should be adjusted for looping")
@ -156,15 +153,15 @@ var invalid_opacity_result = param_anim.set_param("opacity", 300) # Invalid: ab
assert(valid_priority_result == true, "Valid priority parameter should succeed")
assert(valid_color_result == true, "Valid color parameter should succeed")
# Test render method (base implementation should do nothing)
# Test render method (base implementation renders the color)
# Note: is_running check is now in engine_proxy, not in base render()
# Create a frame buffer for testing
var frame = animation.frame_buffer(10)
result = setter_anim.render(frame, engine.time_ms)
assert(result == false, "Base render method should return false")
var render_result = setter_anim.render(frame, engine.time_ms, engine.strip_length)
assert(render_result == true, "Base render method should return true (renders color)")
# Test tostring method
var anim_str = str(anim)
assert(string.find(anim_str, "Animation") >= 0, "String representation should contain 'Animation'")
assert(string.find(anim_str, anim.name) >= 0, "String representation should contain the animation name")
print("All Animation tests passed!")

View File

@ -85,7 +85,7 @@ def run_tests()
pulse.back_color = 0xFF000000 # Transparent
pulse.start()
var rendered = pulse.render(frame, engine.time_ms)
var rendered = pulse.render(frame, engine.time_ms, engine.strip_length)
test_assert(rendered, "Render returns true when running")
# Check that pixels 3 and 4 are red, others are transparent
@ -97,7 +97,7 @@ def run_tests()
# Test 6: Frame rendering with background
frame.clear()
pulse.back_color = 0xFF000080 # Dark blue background
pulse.render(frame, engine.time_ms)
pulse.render(frame, engine.time_ms, engine.strip_length)
test_assert(frame.get_pixel_color(0) == 0xFF000080, "Background pixel is dark blue")
test_assert(frame.get_pixel_color(3) == 0xFFFF0000, "Pulse pixel overrides background")
@ -109,7 +109,7 @@ def run_tests()
pulse.pos = 4
pulse.beacon_size = 2
pulse.slew_size = 1
pulse.render(frame, engine.time_ms)
pulse.render(frame, engine.time_ms, engine.strip_length)
# Check main pulse
test_assert(frame.get_pixel_color(4) == 0xFFFF0000, "Main pulse pixel 1 is red")
@ -128,7 +128,7 @@ def run_tests()
pulse.pos = 0
pulse.beacon_size = 2
pulse.slew_size = 1
pulse.render(frame, engine.time_ms)
pulse.render(frame, engine.time_ms, engine.strip_length)
test_assert(frame.get_pixel_color(0) == 0xFFFF0000, "Pulse at start boundary works")
test_assert(frame.get_pixel_color(1) == 0xFFFF0000, "Pulse at start boundary works")
@ -137,7 +137,7 @@ def run_tests()
pulse.pos = 8
pulse.beacon_size = 2
pulse.slew_size = 1
pulse.render(frame, engine.time_ms)
pulse.render(frame, engine.time_ms, engine.strip_length)
test_assert(frame.get_pixel_color(8) == 0xFFFF0000, "Pulse at end boundary works")
test_assert(frame.get_pixel_color(9) == 0xFFFF0000, "Pulse at end boundary works")
@ -147,7 +147,7 @@ def run_tests()
pulse.pos = 5
pulse.beacon_size = 0
pulse.slew_size = 2
pulse.render(frame, engine.time_ms)
pulse.render(frame, engine.time_ms, engine.strip_length)
# Should have slew on both sides but no main pulse
var left_slew1 = frame.get_pixel_color(3)

View File

@ -26,7 +26,6 @@ def test_atomic_closure_batch_execution()
red_anim.priority = 0
red_anim.duration = 0
red_anim.loop = true
red_anim.name = "red"
var blue_provider = animation.static_color(engine)
blue_provider.color = 0xFF0000FF
@ -35,7 +34,6 @@ def test_atomic_closure_batch_execution()
blue_anim.priority = 0
blue_anim.duration = 0
blue_anim.loop = true
blue_anim.name = "blue"
# Simple test - just verify the basic functionality works
# We'll check that closures execute and animations transition properly
@ -96,7 +94,6 @@ def test_multiple_consecutive_closures()
green_anim.priority = 0
green_anim.duration = 0
green_anim.loop = true
green_anim.name = "green"
var yellow_provider = animation.static_color(engine)
yellow_provider.color = 0xFFFFFF00
@ -105,7 +102,6 @@ def test_multiple_consecutive_closures()
yellow_anim.priority = 0
yellow_anim.duration = 0
yellow_anim.loop = true
yellow_anim.name = "yellow"
# Track closure execution order
var closure_order = []
@ -165,7 +161,6 @@ def test_closure_batch_at_sequence_start()
purple_anim.priority = 0
purple_anim.duration = 0
purple_anim.loop = true
purple_anim.name = "purple"
# Track initial closure execution
var initial_setup_done = false
@ -204,7 +199,6 @@ def test_repeat_sequence_closure_batching()
cyan_anim.priority = 0
cyan_anim.duration = 0
cyan_anim.loop = true
cyan_anim.name = "cyan"
# Track iteration state
var iteration_count = 0
@ -275,7 +269,6 @@ def test_black_frame_fix_integration()
shutter_anim.priority = 0
shutter_anim.duration = 0
shutter_anim.loop = true
shutter_anim.name = "shutter"
# Simulate color cycle (like col1.next = 1)
var color_index = 0

View File

@ -19,12 +19,10 @@ def test_bounce_animation_basic()
# Create a simple source animation
var source = animation.solid(engine)
source.color = 0xFFFF0000
source.name = "test_source"
# Test with default parameters using new parameterized pattern
var bounce_anim = animation.bounce_animation(engine)
bounce_anim.source_animation = source
bounce_anim.name = "test_bounce"
assert(bounce_anim != nil, "BounceAnimation should be created")
assert(bounce_anim.bounce_speed == 128, "Default bounce_speed should be 128")
@ -46,7 +44,6 @@ def test_bounce_animation_custom()
var source = animation.solid(engine)
source.color = 0xFF00FF00
source.name = "test_source"
# Test with custom parameters using new parameterized pattern
var bounce_anim = animation.bounce_animation(engine)
@ -59,7 +56,6 @@ def test_bounce_animation_custom()
bounce_anim.duration = 5000
bounce_anim.loop = false
bounce_anim.opacity = 200
bounce_anim.name = "custom_bounce"
assert(bounce_anim.bounce_speed == 200, "Custom bounce_speed should be 200")
assert(bounce_anim.bounce_range == 15, "Custom bounce_range should be 15")
@ -83,11 +79,9 @@ def test_bounce_animation_parameters()
var source = animation.solid(engine)
source.color = 0xFF0000FF
source.name = "test_source"
var bounce_anim = animation.bounce_animation(engine)
bounce_anim.source_animation = source
bounce_anim.name = "param_test"
# Test parameter changes using virtual member assignment
bounce_anim.bounce_speed = 180
@ -118,7 +112,6 @@ def test_bounce_animation_physics()
var source = animation.solid(engine)
source.color = 0xFFFFFF00
source.name = "test_source"
var bounce_anim = animation.bounce_animation(engine)
bounce_anim.source_animation = source
@ -126,7 +119,6 @@ def test_bounce_animation_physics()
bounce_anim.bounce_range = 0
bounce_anim.damping = 250
bounce_anim.gravity = 0
bounce_anim.name = "physics_test"
# Start animation
bounce_anim.start(1000)
@ -158,7 +150,6 @@ def test_bounce_animation_update_render()
var source = animation.solid(engine)
source.color = 0xFFFF00FF
source.name = "test_source"
var bounce_anim = animation.bounce_animation(engine)
bounce_anim.source_animation = source
@ -166,7 +157,6 @@ def test_bounce_animation_update_render()
bounce_anim.bounce_range = 0
bounce_anim.damping = 250
bounce_anim.gravity = 0
bounce_anim.name = "update_test"
var frame = animation.frame_buffer(10)
@ -175,11 +165,11 @@ def test_bounce_animation_update_render()
assert(bounce_anim.is_running == true, "Animation should be running after start")
# Test update
var result = bounce_anim.update(1500)
assert(result == true, "Update should return true for running animation")
bounce_anim.update(1500)
assert(bounce_anim.is_running == true, "Animation should still be running after update")
# Test render
result = bounce_anim.render(frame, 1500)
var result = bounce_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that frame was modified (colors should be set)
@ -207,7 +197,6 @@ def test_bounce_constructors()
var source = animation.solid(engine)
source.color = 0xFF00FFFF
source.name = "test_source"
# Test bounce_basic
var basic_bounce = animation.bounce_basic(engine)
@ -247,7 +236,6 @@ def test_bounce_animation_gravity()
var source = animation.solid(engine)
source.color = 0xFFFFFFFF
source.name = "test_source"
var gravity_bounce = animation.bounce_animation(engine)
gravity_bounce.source_animation = source
@ -255,7 +243,6 @@ def test_bounce_animation_gravity()
gravity_bounce.bounce_range = 0
gravity_bounce.damping = 240
gravity_bounce.gravity = 100
gravity_bounce.name = "gravity_test"
gravity_bounce.start(1000)
@ -282,7 +269,6 @@ def test_bounce_tostring()
var source = animation.solid(engine)
source.color = 0xFF888888
source.name = "test_source"
var bounce_anim = animation.bounce_animation(engine)
bounce_anim.source_animation = source
@ -290,7 +276,6 @@ def test_bounce_tostring()
bounce_anim.bounce_range = 10
bounce_anim.damping = 240
bounce_anim.gravity = 30
bounce_anim.name = "string_test"
var str_repr = str(bounce_anim)

View File

@ -107,7 +107,7 @@ print(f"Color at full cycle: 0x{color_full :08x}")
# Test rendering
var frame = animation.frame_buffer(5)
blue_breathe.render(frame, engine.time_ms)
blue_breathe.render(frame, engine.time_ms, engine.strip_length)
print(f"First pixel after rendering: 0x{frame.get_pixel_color(0) :08x}")
# Test parameter validation

View File

@ -61,7 +61,6 @@ class ColorCycleAnimationTest
anim.duration = 0
anim.loop = false
anim.opacity = 255
anim.name = "test_default"
# Check that the color was set correctly
self.assert_equal(anim.color != nil, true, "Color is set")
@ -80,7 +79,6 @@ class ColorCycleAnimationTest
anim2.duration = 0
anim2.loop = false
anim2.opacity = 255
anim2.name = "test_custom"
# Check that the color was set correctly
self.assert_equal(anim2.color != nil, true, "Custom color is set")
@ -105,7 +103,6 @@ class ColorCycleAnimationTest
anim.duration = 0
anim.loop = false
anim.opacity = 255
anim.name = "test_render"
# Create a frame buffer
var frame = animation.frame_buffer(10) # 10 pixels
@ -115,26 +112,26 @@ class ColorCycleAnimationTest
# Test brutal color switching - colors should change abruptly, not smoothly
anim.update(0)
anim.render(frame, engine.time_ms)
anim.render(frame, engine.time_ms, engine.strip_length)
var pixel_color = frame.get_pixel_color(0)
self.assert_equal(pixel_color != 0, true, "Start color is not zero")
# Test at middle of cycle - should still be a solid color (brutal switching)
anim.update(500) # 50% through cycle
anim.render(frame, engine.time_ms)
anim.render(frame, engine.time_ms, engine.strip_length)
pixel_color = frame.get_pixel_color(0)
self.assert_equal(pixel_color != 0, true, "Middle color is not zero")
# Test at end of cycle - should be a different solid color
anim.update(1000) # 100% through cycle
anim.render(frame, engine.time_ms)
anim.render(frame, engine.time_ms, engine.strip_length)
pixel_color = frame.get_pixel_color(0)
self.assert_equal(pixel_color != 0, true, "End color is not zero")
# Test manual next color trigger
var initial_color = pixel_color
provider.next = 1 # Trigger move to next color
anim.render(frame, engine.time_ms)
anim.render(frame, engine.time_ms, engine.strip_length)
var next_color = frame.get_pixel_color(0)
# Color should change when next is triggered (though it might be the same if cycling back)
self.assert_equal(next_color != 0, true, "Next color is not zero")
@ -154,7 +151,6 @@ class ColorCycleAnimationTest
manual_anim.duration = 0
manual_anim.loop = false
manual_anim.opacity = 255
manual_anim.name = "manual_test"
# Create a frame buffer
var frame = animation.frame_buffer(10) # 10 pixels
@ -164,20 +160,20 @@ class ColorCycleAnimationTest
# Test that color doesn't change with time in manual mode
manual_anim.update(0)
manual_anim.render(frame, engine.time_ms)
manual_anim.render(frame, engine.time_ms, engine.strip_length)
var initial_color = frame.get_pixel_color(0)
self.assert_equal(initial_color != 0, true, "Initial color should not be zero")
# Advance time significantly - color should NOT change in manual mode
engine.time_ms = 10000 # 10 seconds later
manual_anim.update(engine.time_ms)
manual_anim.render(frame, engine.time_ms)
manual_anim.render(frame, engine.time_ms, engine.strip_length)
var same_color = frame.get_pixel_color(0)
self.assert_equal(same_color, initial_color, "Color should not change with time in manual mode")
# Manually trigger next color
manual_provider.next = 1
manual_anim.render(frame, engine.time_ms)
manual_anim.render(frame, engine.time_ms, engine.strip_length)
var next_color = frame.get_pixel_color(0)
# Color might be the same if we cycled back to the same color, but the trigger should work
self.assert_equal(next_color != 0, true, "Next color should not be zero")
@ -185,7 +181,7 @@ class ColorCycleAnimationTest
# Trigger next again to ensure it works multiple times
var previous_color = next_color
manual_provider.next = 1
manual_anim.render(frame, engine.time_ms)
manual_anim.render(frame, engine.time_ms, engine.strip_length)
var third_color = frame.get_pixel_color(0)
self.assert_equal(third_color != 0, true, "Third color should not be zero")
@ -204,7 +200,6 @@ class ColorCycleAnimationTest
anim.duration = 0
anim.loop = false
anim.opacity = 255
anim.name = "test_direct"
# Check that the animation was created correctly
self.assert_equal(anim != nil, true, "Animation was created")

View File

@ -65,7 +65,6 @@ comet.direction = -1
comet.wrap_around = 0
comet.fade_factor = 150
comet.priority = 15
comet.name = "test_comet"
assert_equals(comet.color, 0xFFFF0000, "Color should be set correctly")
assert_equals(comet.tail_length, 8, "Tail length should be set correctly")
@ -74,7 +73,6 @@ assert_equals(comet.direction, -1, "Direction should be set correctly")
assert_equals(comet.wrap_around, 0, "Wrap around should be disabled")
assert_equals(comet.fade_factor, 150, "Fade factor should be set correctly")
assert_equals(comet.priority, 15, "Priority should be set correctly")
assert_equals(comet.name, "test_comet", "Name should be set correctly")
# Test 2: Multiple Comet Animations
print("\n--- Test 2: Multiple Comet Animations ---")
@ -149,8 +147,10 @@ pos_comet.tail_length = 3
pos_comet.speed = 2560 # 10 pixels/sec (10 * 256)
# Use engine time for testing
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
engine.time_ms = 1000
var start_time = engine.time_ms
pos_comet.start_time = start_time # Set start_time manually for direct testing
pos_comet.start(start_time)
pos_comet.update(start_time)
@ -173,6 +173,7 @@ dir_comet.direction = -1 # Backward
engine.time_ms = 2000
start_time = engine.time_ms
dir_comet.start_time = start_time # Set start_time manually for direct testing
dir_comet.start(start_time)
dir_comet.update(start_time)
var initial_pos = dir_comet.head_position
@ -199,6 +200,7 @@ wrap_comet.wrap_around = 1 # Enable wrapping
small_engine.time_ms = 3000
start_time = small_engine.time_ms
wrap_comet.start_time = start_time # Set start_time manually for direct testing
wrap_comet.start(start_time)
wrap_comet.update(start_time)
small_engine.time_ms = start_time + 2000 # 2 seconds - should wrap multiple times
@ -216,6 +218,7 @@ bounce_comet.wrap_around = 0 # Disable wrapping (enable bouncing)
small_engine.time_ms = 4000
start_time = small_engine.time_ms
bounce_comet.start_time = start_time # Set start_time manually for direct testing
bounce_comet.start(start_time)
bounce_comet.update(small_engine.time_ms)
small_engine.time_ms = start_time + 200 # Should hit the end and bounce
@ -234,12 +237,13 @@ render_comet.tail_length = 3
render_comet.speed = 256 # Slow (1 pixel/sec)
small_engine.time_ms = 5000
render_comet.start_time = small_engine.time_ms # Set start_time manually for direct testing
render_comet.start(small_engine.time_ms)
render_comet.update(small_engine.time_ms)
# Clear frame and render
frame.clear()
var rendered = render_comet.render(frame, small_engine.time_ms)
var rendered = render_comet.render(frame, small_engine.time_ms, small_engine.strip_length)
assert_true(rendered, "Render should return true when successful")
# Check that pixels were set (comet should be at position 0 with tail)
@ -269,6 +273,7 @@ provider_comet.speed = 1280
assert_not_nil(provider_comet, "Comet with color provider should be created")
engine.time_ms = 6000
provider_comet.start_time = engine.time_ms # Set start_time manually for direct testing
provider_comet.start(engine.time_ms)
provider_comet.update(engine.time_ms)
@ -295,6 +300,7 @@ assert_equals(strip_length, 30, "Strip length should come from engine")
# Test engine time usage
engine.time_ms = 7000
engine_comet.start_time = engine.time_ms # Set start_time manually for direct testing
engine_comet.start(engine.time_ms)
engine_comet.update(engine.time_ms)
assert_equals(engine_comet.start_time, 7000, "Animation should use engine time for start")

View File

@ -45,7 +45,6 @@ def run_tests()
crenel.duration = 0
crenel.loop = true
crenel.opacity = 255
crenel.name = "test_crenel"
test_assert(crenel.color == 0xFFFF0000, "Initial color setting")
test_assert(crenel.pos == 4, "Initial position setting")
@ -99,7 +98,7 @@ def run_tests()
crenel.back_color = 0xFF000000 # Transparent
crenel.start()
var rendered = crenel.render(frame, engine.time_ms)
var rendered = crenel.render(frame, engine.time_ms, engine.strip_length)
test_assert(rendered, "Render returns true when running")
# Check pattern: 2 on, 3 off, 2 on, 3 off...
@ -118,7 +117,7 @@ def run_tests()
# Test 6: Frame rendering with background
frame.clear()
crenel.back_color = 0xFF000080 # Dark blue background
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
test_assert(frame.get_pixel_color(2) == 0xFF000080, "Gap pixel has background color")
test_assert(frame.get_pixel_color(0) == 0xFFFF0000, "Pulse pixel overrides background")
@ -127,7 +126,7 @@ def run_tests()
frame.clear()
crenel.back_color = 0xFF000000 # Transparent background
crenel.nb_pulse = 2 # Only 2 pulses
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
# Should have 2 pulses: positions 0,1 and 5,6
test_assert(frame.get_pixel_color(0) == 0xFFFF0000, "Limited pulse 1 pixel 1 is red")
@ -143,7 +142,7 @@ def run_tests()
frame.clear()
crenel.pos = 2 # Start at position 2
crenel.nb_pulse = -1 # Back to infinite
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
# Pattern should start at position 2: positions 2,3 and 7,8
test_assert(frame.get_pixel_color(0) == 0x00000000, "Offset pattern - position 0 is transparent")
@ -158,7 +157,7 @@ def run_tests()
frame.clear()
crenel.pos = 0
crenel.pulse_size = 0
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
# All pixels should remain transparent
for i:0..9
@ -169,7 +168,7 @@ def run_tests()
frame.clear()
crenel.pulse_size = 1
crenel.low_size = 2
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
# Pattern: 1 on, 2 off, 1 on, 2 off...
# Positions: 0 = red, 1,2 = transparent, 3 = red, 4,5 = transparent, 6 = red, 7,8 = transparent, 9 = red
@ -185,7 +184,7 @@ def run_tests()
crenel.pulse_size = 2
crenel.low_size = 3
crenel.pos = -1
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
# With period = 5 and pos = -1, the pattern should be shifted
# The algorithm should handle negative positions correctly
@ -202,7 +201,7 @@ def run_tests()
frame.clear()
crenel.pos = 0
crenel.nb_pulse = 0
crenel.render(frame, engine.time_ms)
crenel.render(frame, engine.time_ms, engine.strip_length)
# All pixels should remain transparent
for i:0..9
@ -235,7 +234,7 @@ def run_tests()
crenel.pulse_size = 10
crenel.low_size = 5
crenel.nb_pulse = 3 # 3 pulses
crenel.render(large_frame)
crenel.render(large_frame, engine.time_ms, 100) # Use frame size as strip_length for this test
# Should have 3 pulses of 10 pixels each, separated by 5 pixels
# Pulse 1: 0-9, Gap: 10-14, Pulse 2: 15-24, Gap: 25-29, Pulse 3: 30-39

View File

@ -31,13 +31,12 @@ def test_crenel_with_integer_color()
crenel.duration = 0 # infinite
crenel.loop = true
crenel.opacity = 255
crenel.name = "test_crenel_int"
# Start and render
crenel.start()
crenel.update(1000)
frame.clear()
var result = crenel.render(frame, engine.time_ms)
var result = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result == true, "Render should succeed with integer color")
assert(crenel.is_running == true, "Animation should be running")
@ -74,13 +73,12 @@ def test_crenel_with_color_provider()
crenel.duration = 0 # infinite
crenel.loop = true
crenel.opacity = 255
crenel.name = "test_crenel_provider"
# Start and render
crenel.start()
crenel.update(1000)
frame.clear()
var result = crenel.render(frame, engine.time_ms)
var result = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result == true, "Render should succeed with ColorProvider")
assert(crenel.is_running == true, "Animation should be running")
@ -117,7 +115,6 @@ def test_crenel_with_dynamic_color_provider()
crenel.duration = 0 # infinite
crenel.loop = true
crenel.opacity = 255
crenel.name = "test_crenel_dynamic"
# Start and render at different times to verify color changes
crenel.start()
@ -125,14 +122,14 @@ def test_crenel_with_dynamic_color_provider()
# Render at time 0
crenel.update(0)
frame.clear()
var result1 = crenel.render(frame, engine.time_ms)
var result1 = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result1 == true, "First render should succeed")
# Render at time 1000 (different color expected)
engine.time_ms = 1000 # Simulate time passage
crenel.update(1000)
frame.clear()
var result2 = crenel.render(frame, engine.time_ms)
var result2 = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result2 == true, "Second render should succeed")
print("✓ CrenelPositionAnimation with dynamic ColorProvider test passed")
@ -166,13 +163,12 @@ def test_crenel_with_generic_value_provider()
crenel.duration = 0 # infinite
crenel.loop = true
crenel.opacity = 255
crenel.name = "test_crenel_generic"
# Start and render
crenel.start()
crenel.update(1000)
frame.clear()
var result = crenel.render(frame, engine.time_ms)
var result = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result == true, "Render should succeed with generic ValueProvider")
assert(crenel.is_running == true, "Animation should be running")
@ -204,7 +200,6 @@ def test_crenel_set_color_methods()
crenel.duration = 0 # infinite
crenel.loop = true
crenel.opacity = 255
crenel.name = "test_set_color"
crenel.start()
@ -212,7 +207,7 @@ def test_crenel_set_color_methods()
crenel.color = 0xFF00FF00 # Green
crenel.update(1000)
frame.clear()
var result1 = crenel.render(frame, engine.time_ms)
var result1 = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result1 == true, "Render with new integer color should succeed")
# Test setting color provider via direct assignment
@ -221,7 +216,7 @@ def test_crenel_set_color_methods()
crenel.color = yellow_provider
crenel.update(1000)
frame.clear()
var result2 = crenel.render(frame, engine.time_ms)
var result2 = crenel.render(frame, engine.time_ms, engine.strip_length)
assert(result2 == true, "Render with ColorProvider should succeed")
print("✓ CrenelPositionAnimation direct color assignment test passed")
@ -247,7 +242,6 @@ def test_crenel_tostring()
crenel_int.duration = 0
crenel_int.loop = true
crenel_int.opacity = 255
crenel_int.name = "test_tostring_int"
var str_int = str(crenel_int)
# Just verify the string is not empty and contains expected parts
@ -269,7 +263,6 @@ def test_crenel_tostring()
crenel_provider.duration = 0
crenel_provider.loop = true
crenel_provider.opacity = 255
crenel_provider.name = "test_tostring_provider"
var str_provider = str(crenel_provider)
# Just verify the string is not empty

View File

@ -396,7 +396,7 @@ def test_external_function_complex()
'extern function rand_meter\n' +
'extern function breathing_effect\n' +
'palette rainbow = [0xFF0000, 0x00FF00, 0x0000FF]\n' +
'animation meter = palette_meter_animation(value_func = rand_meter)\n' +
'animation meter = palette_meter_animation(level = rand_meter())\n' +
'animation breath = solid(color=blue)\n' +
'breath.opacity = breathing_effect\n' +
'run meter'
@ -550,6 +550,46 @@ def test_external_function_execution()
end
end
# Test duplicate extern function declarations are allowed
def test_duplicate_extern_function()
print("Testing duplicate extern function declarations...")
var dsl_source = 'berry """\n' +
'def dup_func()\n' +
' return 50\n' +
'end\n' +
'"""\n' +
'extern function dup_func\n' +
'extern function dup_func\n' +
'extern function dup_func\n' +
'animation test = solid(color=red)\n' +
'test.opacity = dup_func\n' +
'run test'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for duplicate extern declarations")
# Verify only one registration is generated (not duplicated)
import string
var count = 0
var idx = 0
while true
idx = string.find(berry_code, 'animation.register_user_function("dup_func"', idx)
if idx < 0 break end
count += 1
idx += 1
end
assert(count == 1, f"Should generate exactly one registration, got {count}")
# Verify the function call is still generated correctly
assert(string.find(berry_code, "animation.get_user_function('dup_func')(engine)") >= 0, "Should call external function")
print("✓ Duplicate extern function declarations test passed")
return true
end
# Run all tests
def run_all_berry_block_tests()
print("=== DSL Berry Code Blocks and External Functions Test Suite ===")
@ -575,7 +615,8 @@ def run_all_berry_block_tests()
test_external_error_missing_function_name,
test_external_function_reserved_name_validation,
test_external_function_in_sequences,
test_external_function_execution
test_external_function_execution,
test_duplicate_extern_function
]
var passed = 0

View File

@ -24,11 +24,9 @@ print("✓ Basic creation test passed")
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)
@ -60,15 +58,15 @@ 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")
proxy.update(engine.time_ms)
assert(proxy.is_running == 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)
result = proxy.render(frame, engine.time_ms, engine.strip_length)
# Rendering should work (may or may not modify frame depending on animations)
print("✓ Render engine proxy test passed")
@ -94,12 +92,10 @@ print("✓ Remove child test passed")
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
@ -115,7 +111,6 @@ 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)

View File

@ -37,7 +37,7 @@ class TestAnimation : animation.animation
def update(time_ms)
self.update_called = true
self.update_time = time_ms
return super(self).update(time_ms)
super(self).update(time_ms)
end
def reset_test_state()

View File

@ -8,13 +8,15 @@ import animation
# Create a mock engine for testing
class MockEngine
var time_ms
var strip_length
def init()
self.time_ms = 1000 # Fixed time for testing
self.strip_length = 10 # Mock strip length
end
def get_strip_length()
return 10 # Mock strip length
return self.strip_length
end
# Fake add() method for value provider auto-registration
@ -36,7 +38,6 @@ solid_anim.priority = 10
solid_anim.duration = 0
solid_anim.loop = false # Use boolean instead of integer
solid_anim.opacity = 255
solid_anim.name = "solid_test"
assert(solid_anim != nil, "Failed to create solid animation")
# Start the animation
@ -46,7 +47,7 @@ assert(solid_anim.is_running, "Animation should be running")
# Update and render
solid_anim.update(mock_engine.time_ms)
frame.clear()
var result = solid_anim.render(frame, mock_engine.time_ms)
var result = solid_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Check the color of the first pixel
@ -66,7 +67,6 @@ cycle_anim.priority = 10
cycle_anim.duration = 0
cycle_anim.loop = false # Use boolean instead of integer
cycle_anim.opacity = 255
cycle_anim.name = "cycle_test"
assert(cycle_anim != nil, "Failed to create cycle animation")
# Start the animation
@ -76,7 +76,7 @@ assert(cycle_anim.is_running, "Animation should be running")
# Update and render
cycle_anim.update(mock_engine.time_ms)
frame.clear()
result = cycle_anim.render(frame, mock_engine.time_ms)
result = cycle_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Test 3: animation.solid with a rich palette provider
@ -93,7 +93,6 @@ palette_anim.priority = 10
palette_anim.duration = 0
palette_anim.loop = false # Use boolean instead of integer
palette_anim.opacity = 255
palette_anim.name = "palette_test"
assert(palette_anim != nil, "Failed to create palette animation")
# Start the animation
@ -103,7 +102,7 @@ assert(palette_anim.is_running, "Animation should be running")
# Update and render
palette_anim.update(mock_engine.time_ms)
frame.clear()
result = palette_anim.render(frame, mock_engine.time_ms)
result = palette_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Test 4: animation.solid with a composite provider
@ -124,7 +123,6 @@ composite_anim.priority = 10
composite_anim.duration = 0
composite_anim.loop = false # Use boolean instead of integer
composite_anim.opacity = 255
composite_anim.name = "composite_test"
assert(composite_anim != nil, "Failed to create composite animation")
# Start the animation
@ -134,7 +132,7 @@ assert(composite_anim.is_running, "Animation should be running")
# Update and render
composite_anim.update(mock_engine.time_ms)
frame.clear()
result = composite_anim.render(frame, mock_engine.time_ms)
result = composite_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Test 5: Changing color provider dynamically
@ -145,7 +143,6 @@ dynamic_anim.priority = 10
dynamic_anim.duration = 0
dynamic_anim.loop = false # Use boolean instead of integer
dynamic_anim.opacity = 255
dynamic_anim.name = "dynamic_test"
assert(dynamic_anim != nil, "Failed to create dynamic animation")
# Start the animation
@ -155,7 +152,7 @@ assert(dynamic_anim.is_running, "Animation should be running")
# Update and render with initial color
dynamic_anim.update(mock_engine.time_ms)
frame.clear()
result = dynamic_anim.render(frame, mock_engine.time_ms)
result = dynamic_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Check the color of the first pixel
@ -166,7 +163,7 @@ assert(pixel_color == 0xFF0000FF, f"Expected 0xFF0000FF, got {pixel_color:08X}")
dynamic_anim.color = 0x00FF00FF # Green
dynamic_anim.update(mock_engine.time_ms)
frame.clear()
result = dynamic_anim.render(frame, mock_engine.time_ms)
result = dynamic_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Check the color of the first pixel
@ -177,7 +174,7 @@ assert(pixel_color == 0x00FF00FF, f"Expected 0x00FF00FF, got {pixel_color:08X}")
dynamic_anim.color = cycle_provider
dynamic_anim.update(mock_engine.time_ms)
frame.clear()
result = dynamic_anim.render(frame, mock_engine.time_ms)
result = dynamic_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
print("All tests passed!")

View File

@ -19,7 +19,6 @@ fire.flicker_amount = 100
fire.cooling_rate = 55
fire.sparking_rate = 120
fire.priority = 255
fire.name = "test_fire"
print(f"Created fire animation: {fire}")
print(f"Initial state - running: {fire.is_running}, priority: {fire.priority}")
@ -78,7 +77,7 @@ var frame = animation.frame_buffer(30)
frame.clear()
# Render the fire animation
var rendered = fire.render(frame, engine.time_ms)
var rendered = fire.render(frame, engine.time_ms, engine.strip_length)
print(f"Rendered to frame buffer: {rendered}")
# Check that some pixels have been set (fire should create non-black pixels)
@ -154,7 +153,7 @@ tiny_fire.start()
tiny_engine.time_ms = current_time + 125
tiny_fire.update(current_time + 125)
var tiny_frame = animation.frame_buffer(1)
tiny_fire.render(tiny_frame, tiny_engine.time_ms)
tiny_fire.render(tiny_frame, tiny_engine.time_ms, tiny_engine.strip_length)
print("Tiny fire (1 pixel) created and rendered successfully")
# Zero intensity
@ -167,7 +166,7 @@ dim_fire.start()
dim_engine.time_ms = current_time + 250
dim_fire.update(current_time + 250)
var dim_frame = animation.frame_buffer(10)
dim_fire.render(dim_frame, dim_engine.time_ms)
dim_fire.render(dim_frame, dim_engine.time_ms, dim_engine.strip_length)
print("Dim fire (0 intensity) created and rendered successfully")
print("\n=== Fire Animation Test Complete ===")

View File

@ -31,7 +31,6 @@ def test_get_param_value_with_color_provider()
test_anim.duration = 0
test_anim.loop = false
test_anim.opacity = 255
test_anim.name = "test"
# Create a ColorProvider that we can track calls on
class TrackingColorProvider : animation.color_provider
@ -78,7 +77,6 @@ def test_get_param_value_with_generic_provider()
test_anim.duration = 0
test_anim.loop = false
test_anim.opacity = 255
test_anim.name = "test"
# Create a generic ValueProvider that we can track calls on
class TrackingValueProvider : animation.value_provider
@ -125,7 +123,6 @@ def test_get_param_value_with_context_aware_provider()
test_anim.duration = 0
test_anim.loop = false
test_anim.opacity = 255
test_anim.name = "test"
# Create a ValueProvider that returns different values based on parameter name
class ContextAwareProvider : animation.value_provider
@ -180,7 +177,6 @@ def test_get_param_value_with_static_value()
test_anim.duration = 0
test_anim.loop = false
test_anim.opacity = 255
test_anim.name = "test"
# Set a static value (using the 'opacity' parameter that exists in base Animation)
test_anim.opacity = 123

View File

@ -22,9 +22,7 @@ def test_gradient_creation()
# Test single color gradient
var red_gradient = animation.gradient_animation(engine)
red_gradient.color = 0xFFFF0000
red_gradient.name = "red_gradient"
assert(red_gradient != nil, "Should create red gradient")
assert(red_gradient.name == "red_gradient", "Should set name")
# Test radial gradient
var radial_gradient = animation.gradient_animation(engine)
@ -35,7 +33,6 @@ def test_gradient_creation()
radial_gradient.priority = 10
radial_gradient.duration = 5000
radial_gradient.loop = false
radial_gradient.name = "radial_gradient"
assert(radial_gradient != nil, "Should create radial gradient")
assert(radial_gradient.gradient_type == 1, "Should be radial gradient")
@ -50,7 +47,6 @@ def test_gradient_parameters()
var engine = animation.create_engine(strip)
var gradient = animation.gradient_animation(engine)
gradient.color = 0xFFFFFFFF
gradient.name = "test"
# Test parameter setting via virtual members
gradient.gradient_type = 1
@ -84,16 +80,20 @@ def test_gradient_updates()
var gradient = animation.gradient_animation(engine)
gradient.color = 0xFF00FF00
gradient.movement_speed = 100
gradient.name = "test"
# Start the animation
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
gradient.start_time = 1000 # Set start_time manually for direct testing
gradient.start(1000)
assert(gradient.is_running == true, "Should be running after start")
# Test update at different times
assert(gradient.update(1000) == true, "Should update successfully at start time")
assert(gradient.update(1500) == true, "Should update successfully after 500ms")
assert(gradient.update(2000) == true, "Should update successfully after 1000ms")
gradient.update(1000)
assert(gradient.is_running == true, "Should be running after update at start time")
gradient.update(1500)
assert(gradient.is_running == true, "Should be running after update at 500ms")
gradient.update(2000)
assert(gradient.is_running == true, "Should be running after update at 1000ms")
# Test that movement_speed affects phase_offset
var initial_offset = gradient.phase_offset
@ -113,17 +113,17 @@ def test_gradient_rendering()
var gradient = animation.gradient_animation(engine)
gradient.color = 0xFFFF0000
gradient.movement_speed = 0
gradient.name = "test"
# Create a frame buffer
var frame = animation.frame_buffer(5, 1)
# Start and update the animation
gradient.start_time = 1000 # Set start_time manually for direct testing
gradient.start(1000)
gradient.update(1000)
# Test rendering
var result = gradient.render(frame, 1000)
var result = gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render successfully")
# Test that colors were set (basic check)
@ -176,14 +176,14 @@ def test_gradient_position_calculations()
var linear_gradient = animation.gradient_animation(engine)
linear_gradient.color = 0xFFFFFFFF
linear_gradient.movement_speed = 0
linear_gradient.name = "test"
linear_gradient.start_time = 1000 # Set start_time manually for direct testing
linear_gradient.start(1000)
linear_gradient.update(1000)
# The _calculate_linear_position method is private, but we can test the overall effect
# by checking that different pixels get different colors in a linear gradient
var frame = animation.frame_buffer(10, 1)
linear_gradient.render(frame, 1000)
linear_gradient.render(frame, 1000, engine.strip_length)
var first_color = frame.get_pixel_color(0)
var last_color = frame.get_pixel_color(9)
@ -195,10 +195,10 @@ def test_gradient_position_calculations()
radial_gradient.color = 0xFFFFFFFF
radial_gradient.gradient_type = 1
radial_gradient.movement_speed = 0
radial_gradient.name = "test"
radial_gradient.start_time = 1000 # Set start_time manually for direct testing
radial_gradient.start(1000)
radial_gradient.update(1000)
radial_gradient.render(frame, 1000)
radial_gradient.render(frame, 1000, engine.strip_length)
# In a radial gradient, center pixel should be different from edge pixels
var center_color = frame.get_pixel_color(5) # Middle pixel
@ -217,13 +217,11 @@ def test_gradient_color_refactoring()
# Test with static color
var static_gradient = animation.gradient_animation(engine)
static_gradient.color = 0xFFFF0000
static_gradient.name = "static_test"
assert(static_gradient.color == 0xFFFF0000, "Should have color set")
# Test with nil color (default rainbow)
var rainbow_gradient = animation.gradient_animation(engine)
rainbow_gradient.color = nil
rainbow_gradient.name = "rainbow_test"
assert(rainbow_gradient.color == nil, "Should accept nil color for rainbow")
# Test color resolution
@ -232,14 +230,16 @@ def test_gradient_color_refactoring()
# Test basic rendering with different color types
var frame = animation.frame_buffer(5, 1)
static_gradient.start_time = 1000 # Set start_time manually for direct testing
static_gradient.start(1000)
static_gradient.update(1000)
var result = static_gradient.render(frame, 1000)
var result = static_gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render with static color")
rainbow_gradient.start_time = 1000 # Set start_time manually for direct testing
rainbow_gradient.start(1000)
rainbow_gradient.update(1000)
result = rainbow_gradient.render(frame, 1000)
result = rainbow_gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render with rainbow color")
print("✓ GradientAnimation color refactoring test passed")
@ -252,7 +252,6 @@ def test_gradient_virtual_parameters()
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var gradient = animation.gradient_animation(engine)
gradient.name = "test"
# Test virtual parameter assignment and access
gradient.color = 0xFFFF00FF
@ -289,7 +288,6 @@ def test_gradient_tostring()
var static_gradient = animation.gradient_animation(engine)
static_gradient.color = 0xFFFF0000
static_gradient.movement_speed = 50
static_gradient.name = "static_test"
var str_static = str(static_gradient)
assert(str_static != nil, "Should have string representation")
assert(string.find(str_static, "linear") != -1, "Should mention gradient type")
@ -302,7 +300,6 @@ def test_gradient_tostring()
provider_gradient.color = color_provider
provider_gradient.gradient_type = 1
provider_gradient.movement_speed = 25
provider_gradient.name = "provider_test"
var str_provider = str(provider_gradient)
assert(str_provider != nil, "Should have string representation")
assert(string.find(str_provider, "radial") != -1, "Should mention radial type")

View File

@ -18,7 +18,7 @@ rainbow_gradient.update(1000)
# Create frame and render
var frame = animation.frame_buffer(10, 1)
var result = rainbow_gradient.render(frame, 1000)
var result = rainbow_gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render rainbow gradient successfully")
# Check that different pixels have different colors (rainbow effect)

View File

@ -25,12 +25,12 @@ assert(gradient.movement_speed == 50, "Should set movement speed")
gradient.start(1000)
assert(gradient.is_running == true, "Should be running")
var result = gradient.update(1000)
assert(result == true, "Should update successfully")
gradient.update(1000)
assert(gradient.is_running == true, "Should still be running after update")
# Test rendering
var frame = animation.frame_buffer(5, 1)
result = gradient.render(frame, 1000)
result = gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render successfully")
print("✓ Basic GradientAnimation test passed!")

View File

@ -158,11 +158,11 @@ def test_jitter_animation_update_render()
jitter_anim.start(1000)
# Test update
var result = jitter_anim.update(1500)
assert(result == true, "Update should return true for running animation")
jitter_anim.update(1500)
assert(jitter_anim.is_running == true, "Animation should still be running after update")
# Test render
result = jitter_anim.render(frame, 1500)
var result = jitter_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that jitter offsets were initialized

View File

@ -172,10 +172,10 @@ def test_motion_effects_update_render()
bounce_anim.start(1000)
assert(bounce_anim.is_running == true, "Bounce animation should be running after start")
var result = bounce_anim.update(1500)
assert(result == true, "Bounce update should return true for running animation")
bounce_anim.update(1500)
assert(bounce_anim.is_running == true, "Bounce animation should still be running after update")
result = bounce_anim.render(frame, 1500)
var result = bounce_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Bounce render should return true for running animation")
# Test scale update/render
@ -190,10 +190,10 @@ def test_motion_effects_update_render()
scale_anim.start(2000)
assert(scale_anim.is_running == true, "Scale animation should be running after start")
result = scale_anim.update(2500)
assert(result == true, "Scale update should return true for running animation")
scale_anim.update(2500)
assert(scale_anim.is_running == true, "Scale animation should still be running after update")
result = scale_anim.render(frame, 2500)
result = scale_anim.render(frame, 2500, engine.strip_length)
assert(result == true, "Scale render should return true for running animation")
# Test jitter update/render
@ -209,10 +209,10 @@ def test_motion_effects_update_render()
jitter_anim.start(3000)
assert(jitter_anim.is_running == true, "Jitter animation should be running after start")
result = jitter_anim.update(3500)
assert(result == true, "Jitter update should return true for running animation")
jitter_anim.update(3500)
assert(jitter_anim.is_running == true, "Jitter animation should still be running after update")
result = jitter_anim.render(frame, 3500)
result = jitter_anim.render(frame, 3500, engine.strip_length)
assert(result == true, "Jitter render should return true for running animation")
print("✓ Motion effects update/render test passed")

View File

@ -101,15 +101,17 @@ def test_noise_animation_update_render()
var frame = animation.frame_buffer(10)
# Start animation
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
noise_anim.start_time = 1000 # Set start_time manually for direct testing
noise_anim.start(1000)
assert(noise_anim.is_running == true, "Animation should be running after start")
# Test update
var result = noise_anim.update(1500)
assert(result == true, "Update should return true for running animation")
noise_anim.update(1500)
assert(noise_anim.is_running == true, "Animation should still be running after update")
# Test render
result = noise_anim.render(frame, 1500)
var result = noise_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that colors were set (should not all be black)

View File

@ -0,0 +1,236 @@
# Test suite for GradientMeterAnimation (palette_meter_animation)
#
# Tests the VU meter style animation with gradient colors and peak hold.
import animation
# Test basic creation
def test_gradient_meter_creation()
print("Testing GradientMeterAnimation creation...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
assert(meter != nil, "Should create gradient meter animation")
assert(meter.level == 255, "Should default to level 255")
assert(meter.peak_hold == 1000, "Should default to peak_hold 1000ms")
assert(meter.shift_period == 0, "Should default to static gradient")
print("✓ GradientMeterAnimation creation test passed")
end
# Test level parameter
def test_gradient_meter_level()
print("Testing GradientMeterAnimation level...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
# Test level setting
meter.level = 128
assert(meter.level == 128, "Should set level to 128")
meter.level = 255
assert(meter.level == 255, "Should set level to 255")
meter.level = 0
assert(meter.level == 0, "Should set level to 0")
# Test validation
assert(meter.set_param("level", 300) == false, "Should reject level > 255")
assert(meter.set_param("level", -1) == false, "Should reject level < 0")
print("✓ GradientMeterAnimation level test passed")
end
# Test peak hold functionality
def test_gradient_meter_peak_hold()
print("Testing GradientMeterAnimation peak hold...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
# Enable peak hold
meter.peak_hold = 1000 # 1 second hold
assert(meter.peak_hold == 1000, "Should set peak_hold to 1000ms")
# Start animation
meter.start_time = 1000
meter.start(1000)
# Set initial level and update at time 1000
meter.level = 200
meter.update(1000)
assert(meter.peak_level == 200, "Peak should track level")
# peak_time is now 1000
# Lower level at time 1500 - peak should stay (500ms elapsed, within 1000ms hold)
meter.level = 100
meter.update(1500)
assert(meter.peak_level == 200, "Peak should hold at 200")
# At time 2100 (1100ms since peak was set at 1000), hold has expired
meter.update(2100)
assert(meter.peak_level == 100, "Peak should drop to current level after hold expires")
print("✓ GradientMeterAnimation peak hold test passed")
end
# Test rendering
def test_gradient_meter_rendering()
print("Testing GradientMeterAnimation rendering...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
# Use a color provider (required for PaletteGradientAnimation)
var color_source = animation.rich_palette(engine)
meter.color_source = color_source
var frame = animation.frame_buffer(10, 1)
# Start and update (color_source needs update to init LUT)
meter.start_time = 0
meter.start(0)
color_source.update(0)
meter.update(0)
# Test 50% level (should fill ~5 pixels)
meter.level = 128
color_source.update(100)
meter.update(100)
meter.render(frame, 100, 10)
# First pixels should have color, last pixels should be transparent/black
var first_color = frame.get_pixel_color(0)
assert(first_color != 0x00000000, "First pixel should have color")
# Test 0% level (nothing filled)
frame.fill_pixels(frame.pixels, 0x00000000) # Clear frame
meter.level = 0
color_source.update(200)
meter.update(200)
meter.render(frame, 200, 10)
print("✓ GradientMeterAnimation rendering test passed")
end
# Test peak indicator rendering
def test_gradient_meter_peak_indicator()
print("Testing GradientMeterAnimation peak indicator...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
# Use a color provider
var color_source = animation.rich_palette(engine)
meter.color_source = color_source
meter.peak_hold = 2000
var frame = animation.frame_buffer(10, 1)
# Start animation
meter.start_time = 0
meter.start(0)
color_source.update(0)
# Set high level then lower it
meter.level = 200
meter.update(0)
meter.level = 50
color_source.update(100)
meter.update(100)
# Peak should still be at 200, current at 50
assert(meter.peak_level == 200, "Peak should be at 200")
assert(meter.level == 50, "Level should be at 50")
# Render and check peak pixel is set
meter.render(frame, 100, 10)
# Peak pixel position (200/255 * 9 ≈ 7)
var peak_pixel = tasmota.scale_uint(200, 0, 255, 0, 9)
var peak_color = frame.get_pixel_color(peak_pixel)
assert(peak_color != 0x00000000, "Peak indicator pixel should have color")
print("✓ GradientMeterAnimation peak indicator test passed")
end
# Test with color provider
def test_gradient_meter_with_color_provider()
print("Testing GradientMeterAnimation with color provider...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
# Use a color cycle provider
var color_cycle = animation.color_cycle(engine)
meter.color_source = color_cycle
meter.level = 200
meter.start_time = 0
meter.start(0)
color_cycle.update(0)
meter.update(0)
var frame = animation.frame_buffer(10, 1)
var result = meter.render(frame, 0, 10)
assert(result == true, "Should render with color provider")
print("✓ GradientMeterAnimation with color provider test passed")
end
# Test tostring
def test_gradient_meter_tostring()
print("Testing GradientMeterAnimation tostring...")
import string
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var meter = animation.palette_meter_animation(engine)
meter.level = 150
meter.peak_hold = 500
var s = str(meter)
assert(s != nil, "Should have string representation")
assert(string.find(s, "GradientMeterAnimation") != -1, "Should contain class name")
assert(string.find(s, "level=150") != -1, "Should contain level")
assert(string.find(s, "peak_hold=500") != -1, "Should contain peak_hold")
print("✓ GradientMeterAnimation tostring test passed")
end
# Run all tests
def run_palette_meter_animation_tests()
print("=== GradientMeterAnimation Tests ===")
try
test_gradient_meter_creation()
test_gradient_meter_level()
test_gradient_meter_peak_hold()
test_gradient_meter_rendering()
test_gradient_meter_peak_indicator()
test_gradient_meter_with_color_provider()
test_gradient_meter_tostring()
print("=== All GradientMeterAnimation tests passed! ===")
return true
except .. as e, msg
print(f"Test failed: {e} - {msg}")
raise "test_failed"
end
end
animation.run_palette_meter_animation_tests = run_palette_meter_animation_tests
run_palette_meter_animation_tests()
return run_palette_meter_animation_tests

View File

@ -47,20 +47,21 @@ gradient_anim.priority = 10
gradient_anim.duration = 0
gradient_anim.loop = false
gradient_anim.opacity = 255
gradient_anim.name = "gradient_test"
assert(gradient_anim != nil, "Failed to create gradient animation")
assert(gradient_anim.shift_period == 3000, "Shift period should be 3000")
# Start the animation
gradient_anim.start()
gradient_anim.update() # force first tick
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
gradient_anim.start_time = mock_engine.time_ms # Set start_time manually for direct testing
gradient_anim.start(mock_engine.time_ms)
gradient_anim.update(mock_engine.time_ms) # force first tick
assert(gradient_anim.is_running, "Animation should be running")
# Update and render
gradient_anim.update(mock_engine.time_ms)
frame.clear()
result = gradient_anim.render(frame, mock_engine.time_ms)
result = gradient_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Test parameter changes
@ -78,47 +79,6 @@ assert(gradient_anim.phase_shift == 64, "Phase shift should be updated to 64")
gradient_anim.shift_period = 0
assert(gradient_anim.shift_period == 0, "Shift period should be updated to 0 (static)")
# Test 2: PaletteMeterAnimation
print("Test 2: PaletteMeterAnimation")
var meter_anim = animation.palette_meter_animation(mock_engine)
meter_anim.color_source = mock_color_source
# Create a value function that returns 50% (half the strip)
def meter_value_func(engine, time_ms, animation)
return 50 # 50% of the strip (this is still 0-100 for meter logic)
end
meter_anim.value_func = meter_value_func
meter_anim.priority = 10
meter_anim.duration = 0
meter_anim.loop = false
meter_anim.opacity = 255
meter_anim.name = "meter_test"
assert(meter_anim != nil, "Failed to create meter animation")
# Start the animation
meter_anim.start()
meter_anim.update() # force first tick
assert(meter_anim.is_running, "Animation should be running")
# Update and render
meter_anim.update(mock_engine.time_ms)
frame.clear()
result = meter_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test changing value function
def new_meter_value_func(engine, time_ms, animation)
return 75 # 75% of the strip (this is still 0-100 for meter logic)
end
meter_anim.value_func = new_meter_value_func
meter_anim.update(mock_engine.time_ms)
frame.clear()
result = meter_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test 3: Changing color sources dynamically
print("Test 3: Changing color sources dynamically")
var dynamic_anim = animation.palette_gradient_animation(mock_engine)
@ -127,14 +87,15 @@ dynamic_anim.shift_period = 1000
dynamic_anim.spatial_period = 3
# Start the animation
dynamic_anim.start()
dynamic_anim.update() # force first tick
dynamic_anim.start_time = mock_engine.time_ms # Set start_time manually for direct testing
dynamic_anim.start(mock_engine.time_ms)
dynamic_anim.update(mock_engine.time_ms) # force first tick
assert(dynamic_anim.is_running, "Animation should be running")
# Update and render with initial color source
dynamic_anim.update(mock_engine.time_ms)
frame.clear()
result = dynamic_anim.render(frame, mock_engine.time_ms)
result = dynamic_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Create another mock color source
@ -150,7 +111,7 @@ var mock_color_source2 = MockColorSource2()
dynamic_anim.color_source = mock_color_source2
dynamic_anim.update(mock_engine.time_ms)
frame.clear()
result = dynamic_anim.render(frame, mock_engine.time_ms)
result = dynamic_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Test 4: Parameter validation
@ -188,14 +149,15 @@ rich_anim.color_source = rainbow_color_source
rich_anim.shift_period = 1000
# Start the animation
rich_anim.start()
rich_anim.update() # force first tick
rich_anim.start_time = mock_engine.time_ms # Set start_time manually for direct testing
rich_anim.start(mock_engine.time_ms)
rich_anim.update(mock_engine.time_ms) # force first tick
assert(rich_anim.is_running, "Animation should be running")
# Update and render
rich_anim.update(mock_engine.time_ms)
frame.clear()
result = rich_anim.render(frame, mock_engine.time_ms)
result = rich_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(result, "Render should return true")
# Test 6: Animation timing and synchronization
@ -208,21 +170,12 @@ anim1.color_source = mock_color_source
anim1.shift_period = 1000
anim1.spatial_period = 4
var anim2 = animation.palette_meter_animation(mock_engine)
anim2.color_source = mock_color_source2
def meter_func(engine, time_ms, animation)
return 128
end
anim2.value_func = meter_func
# Start both animations at the same time
anim1.start_time = sync_time # Set start_time manually for direct testing
anim1.start(sync_time)
anim1.update(sync_time) # force first tick
anim2.start(sync_time)
anim2.update(sync_time) # force first tick
assert(anim1.start_time == sync_time, "Animation 1 should have correct start time")
assert(anim2.start_time == sync_time, "Animation 2 should have correct start time")
# Test 7: Animation without color source (should handle gracefully)
print("Test 7: Animation without color source")
@ -231,10 +184,11 @@ no_color_anim.shift_period = 1000
no_color_anim.spatial_period = 3
# Note: no color_source set
no_color_anim.start()
no_color_anim.start_time = mock_engine.time_ms # Set start_time manually for direct testing
no_color_anim.start(mock_engine.time_ms)
no_color_anim.update(mock_engine.time_ms)
frame.clear()
result = no_color_anim.render(frame, mock_engine.time_ms)
result = no_color_anim.render(frame, mock_engine.time_ms, mock_engine.strip_length)
assert(!result, "Render should return false when no color source is set")
# Test 8: String representation

View File

@ -22,7 +22,6 @@ def test_parameter_accepts_value_providers()
test_anim.duration = 0
test_anim.loop = false
test_anim.opacity = 255
test_anim.name = "test"
# Test with static integer value (using existing 'opacity' parameter with range 0-255)
assert(test_anim.set_param("opacity", 42) == true, "Should accept static integer")

View File

@ -16,7 +16,6 @@ def test_plasma_animation_basic()
# Test with default parameters
var plasma_anim = animation.plasma_animation(engine)
plasma_anim.name = "test_plasma"
assert(plasma_anim != nil, "PlasmaAnimation should be created")
assert(plasma_anim.freq_x == 32, "Default freq_x should be 32")
@ -50,7 +49,6 @@ def test_plasma_animation_custom()
plasma_anim.priority = 15
plasma_anim.duration = 5000
plasma_anim.loop = false
plasma_anim.name = "custom_plasma"
assert(plasma_anim.freq_x == 50, "Custom freq_x should be 50")
assert(plasma_anim.freq_y == 40, "Custom freq_y should be 40")
@ -74,7 +72,6 @@ def test_plasma_animation_parameters()
var engine = animation.create_engine(strip)
var plasma_anim = animation.plasma_animation(engine)
plasma_anim.name = "param_test"
# Test parameter changes using virtual member assignment
plasma_anim.freq_x = 60
@ -109,7 +106,6 @@ def test_plasma_animation_update_render()
plasma_anim.freq_x = 40
plasma_anim.freq_y = 30
plasma_anim.time_speed = 60
plasma_anim.name = "update_test"
var frame = animation.frame_buffer(10)
@ -118,11 +114,11 @@ def test_plasma_animation_update_render()
assert(plasma_anim.is_running == true, "Animation should be running after start")
# Test update
var result = plasma_anim.update(1500)
assert(result == true, "Update should return true for running animation")
plasma_anim.update(1500)
assert(plasma_anim.is_running == true, "Animation should still be running after update")
# Test render
result = plasma_anim.render(frame, 1500)
var result = plasma_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that colors were set (should not all be black)
@ -152,7 +148,6 @@ def test_plasma_constructors()
var rainbow_plasma = animation.plasma_rainbow(engine)
assert(rainbow_plasma != nil, "plasma_rainbow should create animation")
assert(rainbow_plasma.time_speed == 50, "Rainbow plasma should have correct time_speed")
assert(rainbow_plasma.name == "plasma_rainbow", "Rainbow plasma should have correct name")
# Test plasma_fast
var fast_plasma = animation.plasma_fast(engine)
@ -179,7 +174,6 @@ def test_plasma_tostring()
plasma_anim.phase_y = 70
plasma_anim.time_speed = 85
plasma_anim.blend_mode = 1
plasma_anim.name = "string_test"
var str_repr = str(plasma_anim)

View File

@ -40,25 +40,6 @@ def test_pull_lexer_basic()
return berry_code == direct_berry_code
end
def test_pull_lexer_template_mode()
print("=== Testing Pull Lexer Template Mode ===")
var template_source = "animation test = solid(color=red)\n" +
"test.opacity = 200\n" +
"run test"
# Test with template mode enabled
var pull_lexer = animation_dsl.create_lexer(template_source)
var transpiler = animation_dsl.SimpleDSLTranspiler(pull_lexer)
var berry_code = transpiler.transpile_template_body()
print("Template Body Result:")
print(berry_code)
return true
end
def test_pull_lexer_token_access()
print("=== Testing Pull Lexer Token Access Methods ===")
@ -112,7 +93,6 @@ def run_all_tests()
var tests = [
test_pull_lexer_basic,
test_pull_lexer_template_mode,
test_pull_lexer_token_access,
test_pull_lexer_position_info
]
@ -151,7 +131,6 @@ run_all_tests()
return {
"test_pull_lexer_basic": test_pull_lexer_basic,
"test_pull_lexer_template_mode": test_pull_lexer_template_mode,
"test_pull_lexer_token_access": test_pull_lexer_token_access,
"test_pull_lexer_position_info": test_pull_lexer_position_info,
"run_all_tests": run_all_tests

View File

@ -98,7 +98,7 @@ print(f"Color at full cycle: 0x{color_full :08x}")
# Test rendering
var frame = animation.frame_buffer(5)
blue_pulse.render(frame, engine.time_ms)
blue_pulse.render(frame, engine.time_ms, engine.strip_length)
print(f"First pixel after rendering: 0x{frame.get_pixel_color(0) :08x}")
# Validate key test results

View File

@ -22,7 +22,6 @@ print("Created rich palette animation with engine-only constructor")
# Test that it's created successfully
print(f"Animation created: {anim}")
print(f"Animation type: {type(anim)}")
print(f"Animation name: {anim.name}")
# Test 2: Set parameters using virtual member assignment (parameter forwarding)
anim.palette = bytes("00FF0000" "80FFFF00" "FF0000FF") # Red to Yellow to Blue
@ -61,22 +60,22 @@ print(f"Animation running: {anim.is_running}")
# Test 5: Test rendering
var frame = animation.frame_buffer(5)
anim.render(frame, engine.time_ms)
anim.render(frame, engine.time_ms, engine.strip_length)
var pixel_color = frame.get_pixel_color(0)
print(f"Rendered pixel color: {pixel_color}")
# Test 6: Test color changes over time
engine.time_ms = 0
anim.start(0)
anim.render(frame, 0)
anim.render(frame, 0, engine.strip_length)
var color_t0 = frame.get_pixel_color(0)
engine.time_ms = 1500 # Half cycle
anim.render(frame, 1500)
anim.render(frame, 1500, engine.strip_length)
var color_t1500 = frame.get_pixel_color(0)
engine.time_ms = 3000 # Full cycle
anim.render(frame, 3000)
anim.render(frame, 3000, engine.strip_length)
var color_t3000 = frame.get_pixel_color(0)
print(f"Color at t=0: {color_t0}")
@ -124,7 +123,6 @@ print(f"CSS gradient available: {bool(css_gradient)}")
# Validate key test results
assert(anim != nil, "Rich palette animation should be created")
assert(type(anim) == "instance", "Animation should be an instance")
assert(anim.name == "rich_palette", "Animation should have correct default name")
# Test parameter forwarding
assert(anim.cycle_period == 2000, "Cycle period should be forwarded")

View File

@ -11,13 +11,15 @@ import animation
# Create a mock engine for testing
class MockEngine
var time_ms
var strip_length
def init()
self.time_ms = 0 # Start at time 0
self.strip_length = 10 # Mock strip length
end
def get_strip_length()
return 10 # Mock strip length
return self.strip_length
end
def set_time(time)
@ -132,28 +134,28 @@ class RichPaletteAnimationTest
# Test at start - update engine time and get color
mock_engine.set_time(0)
anim.update(0)
anim.render(frame, 0)
anim.render(frame, 0, mock_engine.strip_length)
var pixel_color = frame.get_pixel_color(0)
self.assert_equal(pixel_color != 0, true, "Start color is not zero")
# Test at middle - update engine time and get color
mock_engine.set_time(500)
anim.update(500) # 50% through cycle
anim.render(frame, 500)
anim.render(frame, 500, mock_engine.strip_length)
var middle_color = frame.get_pixel_color(0)
self.assert_equal(middle_color != 0, true, "Middle color is not zero")
# Test at end - update engine time and get color
mock_engine.set_time(1000)
anim.update(1000) # 100% through cycle
anim.render(frame, 1000)
anim.render(frame, 1000, mock_engine.strip_length)
var end_color = frame.get_pixel_color(0)
self.assert_equal(end_color != 0, true, "End color is not zero")
# Test looping - should be back to start color
mock_engine.set_time(2000)
anim.update(2000) # After another full cycle
anim.render(frame, 2000)
anim.render(frame, 2000, mock_engine.strip_length)
var loop_color = frame.get_pixel_color(0)
self.assert_equal(loop_color, pixel_color, "Loop color matches start color")

View File

@ -232,11 +232,11 @@ def test_scale_animation_update_render()
scale_anim.start(1000)
# Test update
var result = scale_anim.update(1500)
assert(result == true, "Update should return true for running animation")
scale_anim.update(1500)
assert(scale_anim.is_running == true, "Animation should still be running after update")
# Test render
result = scale_anim.render(frame, 1500)
var result = scale_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that colors were calculated

View File

@ -34,7 +34,6 @@ def test_multiple_sequence_managers()
red_anim.duration = 0
red_anim.loop = false
red_anim.opacity = 255
red_anim.name = "red"
var green_provider = animation.static_color(engine)
green_provider.color = 0xFF00FF00
@ -44,7 +43,6 @@ def test_multiple_sequence_managers()
green_anim.duration = 0
green_anim.loop = false
green_anim.opacity = 255
green_anim.name = "green"
var blue_provider = animation.static_color(engine)
blue_provider.color = 0xFF0000FF
@ -54,7 +52,6 @@ def test_multiple_sequence_managers()
blue_anim.duration = 0
blue_anim.loop = false
blue_anim.opacity = 255
blue_anim.name = "blue"
# Create different sequences for each manager using fluent interface
seq_manager1.push_play_step(red_anim, 2000)
@ -105,7 +102,6 @@ def test_sequence_manager_coordination()
anim1.duration = 0
anim1.loop = false
anim1.opacity = 255
anim1.name = "anim1"
var provider2 = animation.static_color(engine)
provider2.color = 0xFF00FF00
@ -115,7 +111,6 @@ def test_sequence_manager_coordination()
anim2.duration = 0
anim2.loop = false
anim2.opacity = 255
anim2.name = "anim2"
# Create sequences that will overlap using fluent interface
seq_manager1.push_play_step(anim1, 3000) # 3 seconds
@ -172,7 +167,6 @@ def test_sequence_manager_engine_integration()
test_anim1.duration = 0
test_anim1.loop = false
test_anim1.opacity = 255
test_anim1.name = "test1"
var provider2 = animation.static_color(engine)
provider2.color = 0xFF00FF00
@ -182,7 +176,6 @@ def test_sequence_manager_engine_integration()
test_anim2.duration = 0
test_anim2.loop = false
test_anim2.opacity = 255
test_anim2.name = "test2"
# Create sequences using fluent interface
seq_manager1.push_play_step(test_anim1, 1000)
@ -274,7 +267,6 @@ def test_sequence_manager_clear_all()
test_anim1.duration = 0
test_anim1.loop = false
test_anim1.opacity = 255
test_anim1.name = "test1"
var provider2 = animation.static_color(engine)
provider2.color = 0xFF00FF00
@ -284,7 +276,6 @@ def test_sequence_manager_clear_all()
test_anim2.duration = 0
test_anim2.loop = false
test_anim2.opacity = 255
test_anim2.name = "test2"
# Create sequences using fluent interface
seq_manager1.push_play_step(test_anim1, 5000)
@ -341,7 +332,6 @@ def test_sequence_manager_stress()
test_anim.duration = 0
test_anim.loop = false
test_anim.opacity = 255
test_anim.name = f"anim{i}"
# Create sequence using fluent interface
seq_managers[i].push_play_step(test_anim, (i + 1) * 500) # Different durations

View File

@ -42,7 +42,6 @@ def test_sequence_manager_step_creation()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Test fluent interface step creation
var seq_manager = animation.sequence_manager(engine)
@ -89,7 +88,6 @@ def test_sequence_manager_execution()
anim1.priority = 0
anim1.duration = 0
anim1.loop = true
anim1.name = "anim1"
var color_provider2 = animation.static_color(engine)
color_provider2.color = 0xFF00FF00
@ -98,7 +96,6 @@ def test_sequence_manager_execution()
anim2.priority = 0
anim2.duration = 0
anim2.loop = true
anim2.name = "anim2"
# Create sequence using fluent interface
seq_manager.push_play_step(anim1, 1000)
@ -137,7 +134,6 @@ def test_sequence_manager_timing()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Create simple sequence with timed steps using fluent interface
seq_manager.push_play_step(test_anim, 1000) # 1 second
@ -191,7 +187,6 @@ def test_sequence_manager_step_info()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 2000)
.push_wait_step(1000)
@ -221,7 +216,6 @@ def test_sequence_manager_stop()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 5000)
@ -259,7 +253,6 @@ def test_sequence_manager_is_running()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 1000)
@ -294,7 +287,6 @@ def test_sequence_manager_assignment_steps()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
test_anim.opacity = 255 # Initial opacity
# Create brightness value provider for assignment
@ -353,7 +345,6 @@ def test_sequence_manager_complex_sequence()
red_anim.priority = 0
red_anim.duration = 0
red_anim.loop = true
red_anim.name = "red"
var green_provider = animation.static_color(engine)
green_provider.color = 0xFF00FF00
@ -362,7 +353,6 @@ def test_sequence_manager_complex_sequence()
green_anim.priority = 0
green_anim.duration = 0
green_anim.loop = true
green_anim.name = "green"
var blue_provider = animation.static_color(engine)
blue_provider.color = 0xFF0000FF
@ -371,7 +361,6 @@ def test_sequence_manager_complex_sequence()
blue_anim.priority = 0
blue_anim.duration = 0
blue_anim.loop = true
blue_anim.name = "blue"
# Create complex sequence using fluent interface
seq_manager.push_play_step(red_anim, 1000) # Play red for 1s
@ -436,7 +425,6 @@ def test_sequence_manager_integration()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 1000)
@ -517,7 +505,6 @@ def test_sequence_manager_repeat_execution_with_functions()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test_repeat"
# Create a function that returns repeat count (simulating palette_size)
var repeat_count_func = def (engine) return 3 end
@ -606,7 +593,6 @@ def test_sequence_manager_dynamic_repeat_changes()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "dynamic_test"
# Create dynamic repeat count that changes based on external state
var external_state = {"multiplier": 2}
@ -719,7 +705,6 @@ def test_sequence_manager_zero_iterations()
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
# Track execution count
var execution_count = 0
@ -842,7 +827,6 @@ def test_sequence_manager_boolean_repeat_counts()
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

View File

@ -116,11 +116,11 @@ def test_shift_animation_update_render()
assert(shift_anim.is_running == true, "Animation should be running after start")
# Test update
var result = shift_anim.update(1500)
assert(result == true, "Update should return true for running animation")
shift_anim.update(1500)
assert(shift_anim.is_running == true, "Animation should still be running after update")
# Test render
result = shift_anim.render(frame, 1500)
var result = shift_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that colors were set

View File

@ -25,7 +25,6 @@ anim.priority = 10
anim.duration = 0 # Infinite
anim.loop = false
anim.opacity = 255
anim.name = "test_solid"
print("Set parameters using virtual member assignment")
@ -42,7 +41,6 @@ print(f"Priority: {anim.priority}")
print(f"Opacity: {anim.opacity}")
print(f"Duration: {anim.duration}")
print(f"Loop: {anim.loop}")
print(f"Name: {anim.name}")
# Test 2: Create animation with red color
var red_anim = animation.solid(engine)
@ -56,7 +54,6 @@ blue_anim.priority = 20
blue_anim.duration = 5000
blue_anim.loop = true
blue_anim.opacity = 200
blue_anim.name = "test_blue"
print(f"Blue animation - priority: {blue_anim.priority}, duration: {blue_anim.duration}, loop: {blue_anim.loop}")
# Test 4: Create animation with ValueProvider
@ -69,7 +66,7 @@ print("Green animation with color provider created")
# Test 5: Test rendering
var frame = animation.frame_buffer(5)
red_anim.start() # Uses engine time automatically
red_anim.render(frame, engine.time_ms)
red_anim.render(frame, engine.time_ms, engine.strip_length)
print("Rendering test completed")
# Test 6: Test engine time usage

View File

@ -20,7 +20,6 @@ def test_unified_solid_function()
red_solid.duration = 0
red_solid.loop = false
red_solid.opacity = 255
red_solid.name = "solid"
# Verify it's created successfully
assert(red_solid != nil, "solid() should return a valid object")
@ -31,7 +30,6 @@ def test_unified_solid_function()
assert(red_solid.opacity == 255, "Should have opacity 255")
assert(red_solid.duration == 0, "Should have infinite duration")
assert(red_solid.loop == false, "Should have no looping")
assert(red_solid.name == "solid", "Should have name 'solid'")
print("✅ Basic solid animation creation test passed")
end
@ -46,14 +44,12 @@ def test_solid_with_all_parameters()
blue_solid.duration = 5000
blue_solid.loop = true
blue_solid.opacity = 200
blue_solid.name = "test_blue"
# Verify all parameters are set correctly
assert(blue_solid.priority == 20, "Should have priority 20")
assert(blue_solid.opacity == 200, "Should have opacity 200")
assert(blue_solid.duration == 5000, "Should have duration 5000")
assert(blue_solid.loop == true, "Should have loop enabled")
assert(blue_solid.name == "test_blue", "Should have name 'test_blue'")
print("✅ Solid with all parameters test passed")
end
@ -68,12 +64,10 @@ def test_solid_composition()
green_solid.duration = 0
green_solid.loop = false
green_solid.opacity = 255
green_solid.name = "green_solid"
# Create another animation for comparison (if pulse exists with new API)
var another_solid = animation.solid(engine)
another_solid.color = 0xFFFFFF00 # Yellow
another_solid.name = "yellow_solid"
# Verify both animations are created
assert(green_solid != nil, "Green solid should be created")
@ -97,7 +91,6 @@ def test_solid_color_provider()
yellow_solid.duration = 0
yellow_solid.loop = false
yellow_solid.opacity = 255
yellow_solid.name = "yellow_solid"
# Verify it works with color providers
assert(yellow_solid != nil, "Should create animation with color provider")
@ -120,14 +113,13 @@ def test_solid_rendering()
red_solid.duration = 0
red_solid.loop = false
red_solid.opacity = 255
red_solid.name = "red_solid"
# Create a frame buffer
var frame = animation.frame_buffer(5)
# Start and render the animation (uses engine time)
red_solid.start()
var result = red_solid.render(frame, engine.time_ms)
var result = red_solid.render(frame, engine.time_ms, engine.strip_length)
# Verify rendering worked
assert(result == true, "Render should return true")

View File

@ -49,7 +49,6 @@ def test_sparkle_animation_custom()
sparkle_anim.priority = 15
sparkle_anim.duration = 5000
sparkle_anim.loop = false
sparkle_anim.name = "custom_sparkle"
assert(sparkle_anim.back_color == 0xFF111111, "Custom background should be set")
assert(sparkle_anim.density == 80, "Custom density should be 80")
@ -73,7 +72,6 @@ def test_sparkle_animation_parameters()
var engine = animation.create_engine(strip)
var sparkle_anim = animation.sparkle_animation(engine)
sparkle_anim.name = "param_test"
# Test parameter changes using virtual member assignment
sparkle_anim.density = 100
@ -112,7 +110,6 @@ def test_sparkle_animation_update_render()
sparkle_anim.sparkle_duration = 30
sparkle_anim.min_brightness = 100
sparkle_anim.max_brightness = 255
sparkle_anim.name = "update_test"
var frame = animation.frame_buffer(10)
@ -128,7 +125,7 @@ def test_sparkle_animation_update_render()
end
# Test render
var result = sparkle_anim.render(frame, 1500)
var result = sparkle_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# With high density (255), we should have some sparkles
@ -159,12 +156,10 @@ def test_sparkle_constructors()
var white_sparkle = animation.sparkle_white(engine)
assert(white_sparkle != nil, "sparkle_white should create animation")
assert(white_sparkle.color == 0xFFFFFFFF, "White sparkle should have white color")
assert(white_sparkle.name == "sparkle_white", "White sparkle should have correct name")
# Test sparkle_rainbow
var rainbow_sparkle = animation.sparkle_rainbow(engine)
assert(rainbow_sparkle != nil, "sparkle_rainbow should create animation")
assert(rainbow_sparkle.name == "sparkle_rainbow", "Rainbow sparkle should have correct name")
# Check that color is set to a provider (not the default white color)
var color_param = rainbow_sparkle.get_param("color")
assert(color_param != nil, "Rainbow sparkle should have color parameter set")
@ -190,7 +185,6 @@ def test_sparkle_tostring()
var sparkle_anim = animation.sparkle_animation(engine)
sparkle_anim.density = 75
sparkle_anim.fade_speed = 45
sparkle_anim.name = "string_test"
var str_repr = str(sparkle_anim)

View File

@ -17,13 +17,13 @@ def test_static_value_provider_interface()
var provider = animation.static_value(engine)
# Test default methods
assert(provider.get_value(1000) == nil, "Default get_value should return nil (no value set)")
assert(provider.update(1000) == false, "Update should return false")
assert(provider.produce_value("test", 1000) == nil, "Default produce_value should return nil (no value set)")
provider.update(1000) # update() does not return a value
# Test setting a value
provider.value = 42
assert(provider.get_value(1000) == 42, "Should return set value")
assert(provider.get_value(2000) == 42, "Should return same value regardless of time")
assert(provider.produce_value("test", 1000) == 42, "Should return set value")
assert(provider.produce_value("test", 2000) == 42, "Should return same value regardless of time")
print("✓ StaticValueProvider interface test passed")
end
@ -39,24 +39,24 @@ def test_static_value_provider_types()
# Test with integer
var int_provider = animation.static_value(engine)
int_provider.value = 123
assert(int_provider.get_value(1000) == 123, "Should handle integer values")
assert(int_provider.produce_value("test", 1000) == 123, "Should handle integer values")
# Test with string
var str_provider = animation.static_value(engine)
str_provider.value = "hello"
assert(str_provider.get_value(1000) == "hello", "Should handle string values")
assert(str_provider.produce_value("test", 1000) == "hello", "Should handle string values")
# Test with color (hex value)
var color_provider = animation.static_value(engine)
color_provider.value = 0xFF00FF00
assert(color_provider.get_value(1000) == 0xFF00FF00, "Should handle color values")
assert(color_provider.produce_value("test", 1000) == 0xFF00FF00, "Should handle color values")
print("✓ StaticValueProvider types test passed")
end
# Test universal get_XXX methods via member() construct
def test_universal_get_methods()
print("Testing universal get_XXX methods...")
# Test produce_value method with different parameter names
def test_produce_value_method()
print("Testing produce_value method...")
# Create engine for testing
var strip = global.Leds()
@ -65,29 +65,13 @@ def test_universal_get_methods()
var provider = animation.static_value(engine)
provider.value = 99
# Test various get_XXX methods
var get_pulse_size = provider.("get_pulse_size")
assert(type(get_pulse_size) == "function", "Should return function for get_pulse_size")
assert(get_pulse_size(1000) == 99, "get_pulse_size should return static value")
# Test produce_value with various parameter names - should return same value
assert(provider.produce_value("pulse_size", 1000) == 99, "produce_value should return static value for pulse_size")
assert(provider.produce_value("pos", 1000) == 99, "produce_value should return static value for pos")
assert(provider.produce_value("color", 1000) == 99, "produce_value should return static value for color")
assert(provider.produce_value("any_param", 2000) == 99, "produce_value should return static value for any param name")
var get_pos = provider.("get_pos")
assert(type(get_pos) == "function", "Should return function for get_pos")
assert(get_pos(1000) == 99, "get_pos should return static value")
var get_color = provider.("get_color")
assert(type(get_color) == "function", "Should return function for get_color")
assert(get_color(1000) == 99, "get_color should return static value")
# Test that non-get methods return undefined
try
var other_method = provider.("some_other_method")
# Should return undefined module, not a function
assert(type(other_method) != "function", "Non-get methods should not return functions")
except .. as e
# Exception is also acceptable
end
print("✓ Universal get_XXX methods test passed")
print("✓ produce_value method test passed")
end
# Test comparison operators
@ -149,17 +133,17 @@ def test_value_changes()
var provider = animation.static_value(engine)
# Test initial state
assert(provider.get_value(1000) == nil, "Initial value should be nil")
assert(provider.produce_value("test", 1000) == nil, "Initial value should be nil")
# Test setting and changing values
provider.value = 10
assert(provider.get_value(1000) == 10, "Should return first set value")
assert(provider.produce_value("test", 1000) == 10, "Should return first set value")
provider.value = 20
assert(provider.get_value(1000) == 20, "Should return updated value")
assert(provider.produce_value("test", 1000) == 20, "Should return updated value")
provider.value = "changed"
assert(provider.get_value(1000) == "changed", "Should handle type changes")
assert(provider.produce_value("test", 1000) == "changed", "Should handle type changes")
print("✓ Value changes test passed")
end
@ -189,7 +173,7 @@ def run_static_value_provider_tests()
try
test_static_value_provider_interface()
test_static_value_provider_types()
test_universal_get_methods()
test_produce_value_method()
test_comparison_operators()
test_parameterized_object_integration()
test_value_changes()

View File

@ -75,6 +75,7 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/crenel_position_animation_test.be",
"lib/libesp32/berry_animation/src/tests/beacon_animation_test.be",
"lib/libesp32/berry_animation/src/tests/gradient_animation_test.be",
"lib/libesp32/berry_animation/src/tests/palette_meter_animation_test.be",
"lib/libesp32/berry_animation/src/tests/noise_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/plasma_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/sparkle_animation_test.be",

View File

@ -22,7 +22,6 @@ twinkle.fade_speed = 180
twinkle.min_brightness = 32
twinkle.max_brightness = 255
twinkle.priority = 10
twinkle.name = "test_twinkle"
print(f"Created twinkle animation: {twinkle}")
print(f"Initial state - running: {twinkle.is_running}, priority: {twinkle.priority}")
@ -62,7 +61,7 @@ while dsl_cycle < 10
dsl_twinkle.update(dsl_test_time)
dsl_frame.clear()
dsl_twinkle.render(dsl_frame, dsl_test_time)
dsl_twinkle.render(dsl_frame, dsl_test_time, engine.strip_length)
var dsl_pixels_lit = 0
var i = 0
@ -200,7 +199,7 @@ while cycle < 10
# Clear and render
render_frame.clear()
var rendered = twinkle.render(render_frame, test_time)
var rendered = twinkle.render(render_frame, test_time, engine.strip_length)
# Count non-black pixels
var non_black_pixels = 0
@ -245,7 +244,6 @@ deterministic_twinkle.fade_speed = 100
deterministic_twinkle.min_brightness = 128
deterministic_twinkle.max_brightness = 255
deterministic_twinkle.priority = 10
deterministic_twinkle.name = "deterministic"
deterministic_twinkle.start()
# Force a specific random seed for reproducible results
@ -262,7 +260,7 @@ while det_cycle < 5
deterministic_twinkle.update(det_time)
det_frame.clear()
deterministic_twinkle.render(det_frame, det_time)
deterministic_twinkle.render(det_frame, det_time, engine.strip_length)
var det_non_black = 0
i = 0
@ -351,7 +349,6 @@ high_density_twinkle.fade_speed = 50
high_density_twinkle.min_brightness = 200
high_density_twinkle.max_brightness = 255
high_density_twinkle.priority = 10
high_density_twinkle.name = "high_density"
high_density_twinkle.start()
var hd_frame = animation.frame_buffer(10)
@ -364,7 +361,7 @@ while hd_cycle < 3
high_density_twinkle.update(hd_time)
hd_frame.clear()
high_density_twinkle.render(hd_frame, hd_time)
high_density_twinkle.render(hd_frame, hd_time, engine.strip_length)
var hd_non_black = 0
i = 0
@ -390,7 +387,7 @@ tiny_twinkle.density = 200
tiny_twinkle.start()
tiny_twinkle.update(current_time + 167)
var tiny_frame = animation.frame_buffer(1)
tiny_twinkle.render(tiny_frame)
tiny_twinkle.render(tiny_frame, current_time, tiny_engine.strip_length)
print("Tiny twinkle (1 pixel) created and rendered successfully")
# Zero density
@ -399,7 +396,7 @@ no_twinkle.density = 0
no_twinkle.start()
no_twinkle.update(current_time + 334)
var no_frame = animation.frame_buffer(10)
no_twinkle.render(no_frame)
no_twinkle.render(no_frame, current_time, engine.strip_length)
print("No twinkle (0 density) created and rendered successfully")
# Maximum density
@ -408,7 +405,7 @@ max_twinkle.density = 255
max_twinkle.start()
max_twinkle.update(current_time + 501)
var max_frame = animation.frame_buffer(10)
max_twinkle.render(max_frame)
max_twinkle.render(max_frame, current_time, engine.strip_length)
print("Max twinkle (255 density) created and rendered successfully")
# Test 17: Alpha-Based Fading Verification

View File

@ -110,15 +110,17 @@ def test_wave_animation_update_render()
var frame = animation.frame_buffer(10)
# Start animation
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
wave_anim.start_time = 1000 # Set start_time manually for direct testing
wave_anim.start(1000)
assert(wave_anim.is_running == true, "Animation should be running after start")
# Test update
var result = wave_anim.update(1500)
assert(result == true, "Update should return true for running animation")
wave_anim.update(1500)
assert(wave_anim.is_running == true, "Animation should still be running after update")
# Test render
result = wave_anim.render(frame, 1500)
var result = wave_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that colors were set (should not all be black with high amplitude)
@ -156,9 +158,10 @@ def test_wave_types()
wave_anim.frequency = 50
wave_anim.wave_speed = 0 # No movement for testing
wave_anim.start_time = 1000 # Set start_time manually for direct testing
wave_anim.start(1000)
wave_anim.update(1000)
var result = wave_anim.render(frame, 1000)
var result = wave_anim.render(frame, 1000, engine.strip_length)
assert(result == true, f"Wave type {wave_types[i]} should render successfully")
i += 1