Berry animation improvements to sequence (#23858)

This commit is contained in:
s-hadinger 2025-08-31 23:23:38 +02:00 committed by GitHub
parent d20cebb654
commit 6d4b49c88b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 14684 additions and 12382 deletions

View File

@ -24,7 +24,7 @@ aurora_base_.transition_type = animation.SINE # transition type (explicit for c
aurora_base_.brightness = 180 # brightness (dimmed for aurora effect)
var demo_ = animation.SequenceManager(engine)
.push_play_step(aurora_base_, nil) # infinite duration (no 'for' clause)
engine.add_sequence_manager(demo_)
engine.add(demo_)
engine.start()

View File

@ -39,7 +39,7 @@ breathing_.opacity = (def (engine)
return provider
end)(engine)
# Start the animation
engine.add_animation(breathing_)
engine.add(breathing_)
engine.start()

View File

@ -140,16 +140,16 @@ stripe10_.pos = (def (engine)
return provider
end)(engine)
# Start all stripes
engine.add_animation(stripe1_)
engine.add_animation(stripe2_)
engine.add_animation(stripe3_)
engine.add_animation(stripe4_)
engine.add_animation(stripe5_)
engine.add_animation(stripe6_)
engine.add_animation(stripe7_)
engine.add_animation(stripe8_)
engine.add_animation(stripe9_)
engine.add_animation(stripe10_)
engine.add(stripe1_)
engine.add(stripe2_)
engine.add(stripe3_)
engine.add(stripe4_)
engine.add(stripe5_)
engine.add(stripe6_)
engine.add(stripe7_)
engine.add(stripe8_)
engine.add(stripe9_)
engine.add(stripe10_)
engine.start()

View File

@ -61,11 +61,11 @@ garland_.tail_length = 6 # garland length (tail length)
garland_.speed = 4000 # slow movement (speed)
garland_.priority = 5
# Start all animations
engine.add_animation(tree_base_)
engine.add_animation(ornaments_)
engine.add_animation(tree_star_)
engine.add_animation(snow_sparkles_)
engine.add_animation(garland_)
engine.add(tree_base_)
engine.add(ornaments_)
engine.add(tree_star_)
engine.add(snow_sparkles_)
engine.add(garland_)
engine.start()

View File

@ -36,10 +36,10 @@ comet_sparkles_.density = 8 # density (moderate sparkles)
comet_sparkles_.twinkle_speed = 400 # twinkle speed (quick sparkle)
comet_sparkles_.priority = 8
# Start all animations
engine.add_animation(background_)
engine.add_animation(comet_main_)
engine.add_animation(comet_secondary_)
engine.add_animation(comet_sparkles_)
engine.add(background_)
engine.add(comet_main_)
engine.add(comet_secondary_)
engine.add(comet_sparkles_)
engine.start()

View File

@ -31,8 +31,8 @@ stream2_.priority = 5
stream1_.tail_length = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) / 5 end)
stream2_.opacity = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) * 4 end)
# Run both animations
engine.add_animation(stream1_)
engine.add_animation(stream2_)
engine.add(stream1_)
engine.add(stream2_)
engine.start()

View File

@ -27,7 +27,7 @@ def cylon_effect_template(engine, eye_color_, back_color_, duration_)
eye_animation_.beacon_size = 3 # small 3 pixels eye
eye_animation_.slew_size = 2 # with 2 pixel shading around
eye_animation_.priority = 5
engine.add_animation(eye_animation_)
engine.add(eye_animation_)
end
animation.register_user_function('cylon_effect', cylon_effect_template)

View File

@ -38,11 +38,11 @@ red_eye_.beacon_size = 3 # small 3 pixels eye
red_eye_.slew_size = 2 # with 2 pixel shading around
var cylon_eye_ = animation.SequenceManager(engine, -1)
.push_play_step(red_eye_, eye_duration_) # use COSINE movement
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end) # switch to TRIANGLE
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end) # switch to TRIANGLE
.push_play_step(red_eye_, eye_duration_)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end) # switch back to COSINE for next iteration
.push_assign_step(def (engine) eye_color_.next = 1 end) # advance to next color
engine.add_sequence_manager(cylon_eye_)
.push_closure_step(def (engine) red_eye_.pos = cosine_val_ end) # switch back to COSINE for next iteration
.push_closure_step(def (engine) eye_color_.next = 1 end) # advance to next color
engine.add(cylon_eye_)
engine.start()

View File

@ -24,7 +24,7 @@ red_eye_.pos = (def (engine)
end)(engine)
red_eye_.beacon_size = 3 # small 3 pixels eye
red_eye_.slew_size = 2 # with 2 pixel shading around
engine.add_animation(red_eye_)
engine.add(red_eye_)
engine.start()

View File

@ -18,16 +18,17 @@ fire_color_.palette = fire_colors_
var background_ = animation.solid(engine)
background_.color = 0xFF000088
background_.priority = 20
var eye_mask_ = animation.beacon_animation(engine)
eye_mask_.color = 0x00000000
eye_mask_.back_color = 0xFFFFFFFF
eye_mask_.pos = (def (engine)
var eye_pos_ = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.duration = 3000
provider.duration = 6000
return provider
end)(engine)
var eye_mask_ = animation.beacon_animation(engine)
eye_mask_.color = 0xFFFFFFFF
eye_mask_.back_color = 0x00000000
eye_mask_.pos = eye_pos_
eye_mask_.beacon_size = 4 # small 3 pixels eye
eye_mask_.slew_size = 2 # with 2 pixel shading around
eye_mask_.priority = 5
@ -35,8 +36,8 @@ var fire_pattern_ = animation.palette_gradient_animation(engine)
fire_pattern_.color_source = fire_color_
fire_pattern_.spatial_period = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) / 4 end)
fire_pattern_.opacity = eye_mask_
engine.add_animation(background_)
engine.add_animation(fire_pattern_)
engine.add(background_)
engine.add(fire_pattern_)
engine.start()
@ -57,10 +58,11 @@ color fire_color = rich_palette(palette=fire_colors)
animation background = solid(color=0x000088, priority=20)
run background
set eye_pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 6s)
animation eye_mask = beacon_animation(
color = transparent
back_color = white
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 3s)
color = white
back_color = transparent
pos = eye_pos
beacon_size = 4 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5

View File

@ -0,0 +1,138 @@
# Generated Berry code from Animation DSL
# Source: demo_shutter_rainbow.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_left_right
def shutter_left_right_template(engine, colors_, duration_)
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 = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
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
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 (self) return self.resolve(strip_len_) - self.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine)
#repeat col1.palette_size times {
.push_repeat_subsequence(animation.SequenceManager(engine, 7)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
engine.add(shutter_seq_)
end
animation.register_user_function('shutter_left_right', shutter_left_right_template)
var Violet_ = 0xFF112233
var rainbow_with_white_ = bytes("FFFF0000" "FFFFA500" "FFFFFF00" "FF008000" "FF0000FF" "FF4B0082" "FFFFFFFF")
shutter_left_right_template(engine, rainbow_with_white_, 1500)
engine.start()
#- Original DSL source:
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors
template shutter_left_right {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
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 col1.palette_size times {
repeat 7 times {
reset shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
reset shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq
}
color Violet = 0x112233
palette rainbow_with_white = [
red
orange
yellow
green
blue
indigo
white
]
shutter_left_right(rainbow_with_white, 1.5s)
-#

View File

@ -0,0 +1,84 @@
# Generated Berry code from Animation DSL
# Source: demo_shutter_rainbow2.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var duration_ = 3000
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 = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = animation.PALETTE_RAINBOW
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = animation.PALETTE_RAINBOW
col2_.cycle_period = 0
col2_.next = 1
var shutter_animation_ = animation.beacon_animation(engine)
shutter_animation_.color = col1_
shutter_animation_.back_color = col2_
shutter_animation_.pos = 0
shutter_animation_.beacon_size = shutter_size_
shutter_animation_.slew_size = 0
shutter_animation_.priority = 5
log(f"foobar", 3)
var shutter_run_ = animation.SequenceManager(engine, -1)
.push_closure_step(def (engine) log(f"before", 3) end)
.push_play_step(shutter_animation_, duration_)
.push_closure_step(def (engine) log(f"after", 3) end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
.push_closure_step(def (engine) log(f"next", 3) end)
engine.add(shutter_run_)
engine.start()
#- Original DSL source:
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors
set duration = 3s
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=PALETTE_RAINBOW, cycle_period=0)
color col2 = color_cycle(palette=PALETTE_RAINBOW, cycle_period=0)
col2.next = 1
animation shutter_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
log("foobar")
sequence shutter_run repeat forever {
log("before")
play shutter_animation for duration
log("after")
col1.next = 1
col2.next = 1
log("next")
}
run shutter_run
-#

View File

@ -72,10 +72,10 @@ disco_pulse_.pos = (def (engine)
return provider
end)(engine) # Fast movement
# Start all animations
engine.add_animation(disco_base_)
engine.add_animation(white_flash_)
engine.add_animation(disco_sparkles_)
engine.add_animation(disco_pulse_)
engine.add(disco_base_)
engine.add(white_flash_)
engine.add(disco_sparkles_)
engine.add(disco_pulse_)
engine.start()

View File

@ -40,8 +40,8 @@ fire_flicker_.density = 12 # density (number of flickers)
fire_flicker_.twinkle_speed = 200 # twinkle speed (flicker duration)
fire_flicker_.priority = 10
# Start both animations
engine.add_animation(fire_base_)
engine.add_animation(fire_flicker_)
engine.add(fire_base_)
engine.add(fire_flicker_)
engine.start()

View File

@ -72,11 +72,11 @@ center_pulse_.opacity = (def (engine)
return provider
end)(engine) # Quick white flash
# Start all animations
engine.add_animation(background_)
engine.add_animation(heart_glow_)
engine.add_animation(heartbeat1_)
engine.add_animation(heartbeat2_)
engine.add_animation(center_pulse_)
engine.add(background_)
engine.add(heart_glow_)
engine.add(heartbeat1_)
engine.add(heartbeat2_)
engine.add(center_pulse_)
engine.start()

View File

@ -29,7 +29,7 @@ var import_demo_ = animation.SequenceManager(engine)
.push_play_step(breathing_blue_, 3000)
.push_play_step(dynamic_green_, 3000)
# Run the demo
engine.add_sequence_manager(import_demo_)
engine.add(import_demo_)
engine.start()

View File

@ -87,11 +87,11 @@ heat_shimmer_.density = 6 # density (shimmer points)
heat_shimmer_.twinkle_speed = 1500 # twinkle speed (slow shimmer)
heat_shimmer_.priority = 15
# Start all animations
engine.add_animation(lava_base_)
engine.add_animation(lava_blob1_)
engine.add_animation(lava_blob2_)
engine.add_animation(lava_blob3_)
engine.add_animation(heat_shimmer_)
engine.add(lava_base_)
engine.add(lava_blob1_)
engine.add(lava_blob2_)
engine.add(lava_blob3_)
engine.add(heat_shimmer_)
engine.start()

View File

@ -67,11 +67,11 @@ distant_flash_.density = 4 # density (few flashes)
distant_flash_.twinkle_speed = 300 # twinkle speed (medium duration)
distant_flash_.priority = 5
# Start all animations
engine.add_animation(storm_bg_)
engine.add_animation(lightning_main_)
engine.add_animation(lightning_partial_)
engine.add_animation(afterglow_)
engine.add_animation(distant_flash_)
engine.add(storm_bg_)
engine.add(lightning_main_)
engine.add(lightning_partial_)
engine.add(afterglow_)
engine.add(distant_flash_)
engine.start()

View File

@ -57,11 +57,11 @@ code_flash_.density = 3 # density (few flashes)
code_flash_.twinkle_speed = 150 # twinkle speed (quick flash)
code_flash_.priority = 20
# Start all animations
engine.add_animation(background_)
engine.add_animation(stream1_)
engine.add_animation(stream2_)
engine.add_animation(stream3_)
engine.add_animation(code_flash_)
engine.add(background_)
engine.add(stream1_)
engine.add(stream2_)
engine.add(stream3_)
engine.add(code_flash_)
engine.start()

View File

@ -50,13 +50,13 @@ meteor_flash_.density = 1 # density (single flash)
meteor_flash_.twinkle_speed = 100 # twinkle speed (very quick)
meteor_flash_.priority = 25
# Start all animations
engine.add_animation(background_)
engine.add_animation(stars_)
engine.add_animation(meteor1_)
engine.add_animation(meteor2_)
engine.add_animation(meteor3_)
engine.add_animation(meteor4_)
engine.add_animation(meteor_flash_)
engine.add(background_)
engine.add(stars_)
engine.add(meteor1_)
engine.add(meteor2_)
engine.add(meteor3_)
engine.add(meteor4_)
engine.add(meteor_flash_)
engine.start()

View File

@ -72,12 +72,12 @@ arc_sparkles_.density = 4 # density (few arcs)
arc_sparkles_.twinkle_speed = 100 # twinkle speed (quick arcs)
arc_sparkles_.priority = 15
# Start all animations
engine.add_animation(neon_main_)
engine.add_animation(neon_surge_)
engine.add_animation(segment1_)
engine.add_animation(segment2_)
engine.add_animation(segment3_)
engine.add_animation(arc_sparkles_)
engine.add(neon_main_)
engine.add(neon_surge_)
engine.add(segment1_)
engine.add(segment2_)
engine.add(segment3_)
engine.add(arc_sparkles_)
engine.start()

View File

@ -64,10 +64,10 @@ foam_.density = 6 # density (sparkle count)
foam_.twinkle_speed = 300 # twinkle speed (quick sparkles)
foam_.priority = 15
# Start all animations
engine.add_animation(ocean_base_)
engine.add_animation(wave1_)
engine.add_animation(wave2_)
engine.add_animation(foam_)
engine.add(ocean_base_)
engine.add(wave1_)
engine.add(wave2_)
engine.add(foam_)
engine.start()

View File

@ -33,7 +33,7 @@ var palette_demo_ = animation.SequenceManager(engine)
.push_play_step(fire_anim_, 3000)
.push_play_step(ocean_anim_, 3000)
)
engine.add_sequence_manager(palette_demo_)
engine.add(palette_demo_)
engine.start()

View File

@ -60,7 +60,7 @@ var palette_showcase_ = animation.SequenceManager(engine)
.push_play_step(aurora_lights_, 2000)
.push_play_step(sunset_glow_, 2000)
)
engine.add_sequence_manager(palette_showcase_)
engine.add(palette_showcase_)
engine.start()

View File

@ -84,10 +84,10 @@ plasma_base_.opacity = (def (engine)
return provider
end)(engine)
# Start all animations
engine.add_animation(plasma_base_)
engine.add_animation(plasma_wave1_)
engine.add_animation(plasma_wave2_)
engine.add_animation(plasma_wave3_)
engine.add(plasma_base_)
engine.add(plasma_wave1_)
engine.add(plasma_wave2_)
engine.add(plasma_wave3_)
engine.start()

View File

@ -57,9 +57,9 @@ white_strobe_.opacity = (def (engine)
end)(engine) # Quick bright flashes
white_strobe_.priority = 20
# Start all animations
engine.add_animation(left_red_)
engine.add_animation(right_blue_)
engine.add_animation(white_strobe_)
engine.add(left_red_)
engine.add(right_blue_)
engine.add(white_strobe_)
engine.start()

View File

@ -55,7 +55,7 @@ var demo_ = animation.SequenceManager(engine)
.push_play_step(right_pulse_, 2000)
.push_wait_step(1000)
)
engine.add_sequence_manager(demo_)
engine.add(demo_)
engine.start()

View File

@ -20,7 +20,7 @@ rainbow_cycle_.cycle_period = 5000 # cycle period
var rainbow_animation_ = animation.solid(engine)
rainbow_animation_.color = rainbow_cycle_
# Start the animation
engine.add_animation(rainbow_animation_)
engine.add(rainbow_animation_)
engine.start()

View File

@ -4,7 +4,7 @@
set -e
BERRY_CMD="./berry -s -g -m lib/libesp32/berry_animation/src -e 'import tasmota'"
BERRY_CMD="./berry -s -g -m lib/libesp32/berry_animation/src -e 'import tasmota def log(x) print(x) end '"
COMPILED_DIR="lib/libesp32/berry_animation/anim_examples/compiled"
echo "Running compiled DSL examples..."

View File

@ -48,9 +48,9 @@ end)(engine)
scanner_trail_.pos = pos_test_
scanner_trail_.opacity = 128 # Half brightness
# Start all animations
engine.add_animation(background_)
engine.add_animation(scanner_trail_)
engine.add_animation(scanner_)
engine.add(background_)
engine.add(scanner_trail_)
engine.add(scanner_)
engine.start()

View File

@ -48,36 +48,36 @@ pulse_demo_.priority = 5
# Sequence 1: Cylon Eye with Position Changes
var cylon_eye_ = animation.SequenceManager(engine)
.push_play_step(red_eye_, 3000)
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end) # Change to triangle oscillator
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end) # Change to triangle oscillator
.push_play_step(red_eye_, 3000)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end) # Change back to cosine
.push_assign_step(def (engine) eye_color_.next = 1 end) # Advance to next color
.push_closure_step(def (engine) red_eye_.pos = cosine_val_ end) # Change back to cosine
.push_closure_step(def (engine) eye_color_.next = 1 end) # Advance to next color
.push_play_step(red_eye_, 2000)
# Sequence 2: Brightness Control Demo
var brightness_demo_ = animation.SequenceManager(engine)
.push_play_step(pulse_demo_, 2000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # Dim the animation
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # Dim the animation
.push_play_step(pulse_demo_, 2000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Brighten again
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Brighten again
.push_play_step(pulse_demo_, 2000)
# Sequence 3: Multiple Property Changes
var multi_change_ = animation.SequenceManager(engine)
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFFFF0000 end) # Change color
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # And brightness
.push_closure_step(def (engine) pulse_demo_.color = 0xFFFF0000 end) # Change color
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # And brightness
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFF008000 end) # Change color again
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Full brightness
.push_closure_step(def (engine) pulse_demo_.color = 0xFF008000 end) # Change color again
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Full brightness
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFF0000FF end) # Back to blue
.push_closure_step(def (engine) pulse_demo_.color = 0xFF0000FF end) # Back to blue
# Sequence 4: Assignments in Repeat Blocks
var repeat_demo_ = animation.SequenceManager(engine)
.push_repeat_subsequence(animation.SequenceManager(engine, 3)
.push_play_step(red_eye_, 1000)
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end) # Change oscillator
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end) # Change oscillator
.push_play_step(red_eye_, 1000)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end) # Change back
.push_assign_step(def (engine) eye_color_.next = 1 end) # Next color
.push_closure_step(def (engine) red_eye_.pos = cosine_val_ end) # Change back
.push_closure_step(def (engine) eye_color_.next = 1 end) # Next color
)
# Main demo sequence combining all examples
var main_demo_ = animation.SequenceManager(engine)
@ -85,31 +85,31 @@ var main_demo_ = animation.SequenceManager(engine)
.push_play_step(red_eye_, 1000)
.push_wait_step(500)
# Demonstrate position changes
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end)
.push_closure_step(def (engine) red_eye_.pos = triangle_val_ end)
.push_play_step(red_eye_, 2000)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end)
.push_closure_step(def (engine) red_eye_.pos = cosine_val_ end)
.push_play_step(red_eye_, 2000)
# Color cycling
.push_assign_step(def (engine) eye_color_.next = 1 end)
.push_closure_step(def (engine) eye_color_.next = 1 end)
.push_play_step(red_eye_, 1000)
.push_assign_step(def (engine) eye_color_.next = 1 end)
.push_closure_step(def (engine) eye_color_.next = 1 end)
.push_play_step(red_eye_, 1000)
.push_wait_step(1000)
# Brightness demo with pulse
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end)
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_low_ end)
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end)
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_high_ end)
.push_play_step(pulse_demo_, 1000)
# Multi-property changes
.push_assign_step(def (engine) pulse_demo_.color = 0xFFFF0000 end)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end)
.push_closure_step(def (engine) pulse_demo_.color = 0xFFFF0000 end)
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_low_ end)
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFF008000 end)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end)
.push_closure_step(def (engine) pulse_demo_.color = 0xFF008000 end)
.push_closure_step(def (engine) pulse_demo_.opacity = brightness_high_ end)
.push_play_step(pulse_demo_, 1000)
# Run the main demo
engine.add_sequence_manager(main_demo_)
engine.add(main_demo_)
engine.start()

View File

@ -21,7 +21,7 @@ rainbow_cycle_.cycle_period = 3000
# Simple sequence
var demo_ = animation.SequenceManager(engine)
.push_play_step(rainbow_cycle_, 15000)
engine.add_sequence_manager(demo_)
engine.add(demo_)
engine.start()

View File

@ -74,10 +74,10 @@ stars_.opacity = (def (engine)
return provider
end)(engine) # Fade out during day
# Start all animations
engine.add_animation(daylight_cycle_)
engine.add_animation(sun_position_)
engine.add_animation(sun_glow_)
engine.add_animation(stars_)
engine.add(daylight_cycle_)
engine.add(sun_position_)
engine.add(sun_glow_)
engine.add(stars_)
engine.start()

View File

@ -19,8 +19,8 @@ var swipe_animation_ = animation.solid(engine)
swipe_animation_.color = olivary_
var slide_colors_ = animation.SequenceManager(engine)
.push_play_step(swipe_animation_, 1000)
.push_assign_step(def (engine) olivary_.next = 1 end)
engine.add_sequence_manager(slide_colors_)
.push_closure_step(def (engine) olivary_.next = 1 end)
engine.add(slide_colors_)
engine.start()

View File

@ -26,8 +26,8 @@ def rainbow_pulse_template(engine, pal1_, pal2_, duration_, back_color_)
# Set pulse priority higher
pulse_.priority = 10
# Run both animations
engine.add_animation(background_)
engine.add_animation(pulse_)
engine.add(background_)
engine.add(pulse_)
end
animation.register_user_function('rainbow_pulse', rainbow_pulse_template)

View File

@ -17,7 +17,7 @@ def pulse_effect_template(engine, base_color_, duration_, brightness_)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add_animation(pulse_)
engine.add(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)

View File

@ -17,7 +17,7 @@ def pulse_effect_template(engine, base_color_, duration_, brightness_)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add_animation(pulse_)
engine.add(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)

View File

@ -29,9 +29,9 @@ bright_flash_.density = 2 # density (fewer bright flashes)
bright_flash_.twinkle_speed = 300 # twinkle speed (quick flash)
bright_flash_.priority = 15
# Start all animations
engine.add_animation(background_)
engine.add_animation(stars_)
engine.add_animation(bright_flash_)
engine.add(background_)
engine.add(stars_)
engine.add(bright_flash_)
engine.start()

View File

@ -45,11 +45,11 @@ random_complex_.priority = 20
# Complex expression with user function and math operations
random_complex_.opacity = animation.create_closure_value(engine, def (self) return self.round((animation.get_user_function('rand_demo')(self.engine) + 128) / 2 + self.abs(animation.get_user_function('rand_demo')(self.engine) - 100)) end)
# Run all animations to demonstrate the effects
engine.add_animation(random_base_)
engine.add_animation(random_bounded_)
engine.add_animation(random_variation_)
engine.add_animation(random_multi_)
engine.add_animation(random_complex_)
engine.add(random_base_)
engine.add(random_bounded_)
engine.add(random_variation_)
engine.add(random_multi_)
engine.add(random_complex_)
engine.start()

View File

@ -14,10 +14,11 @@ color fire_color = rich_palette(palette=fire_colors)
animation background = solid(color=0x000088, priority=20)
run background
set eye_pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 6s)
animation eye_mask = beacon_animation(
color = transparent
back_color = white
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = 3s)
color = white
back_color = transparent
pos = eye_pos
beacon_size = 4 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5

View File

@ -0,0 +1,65 @@
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors
template shutter_left_right {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
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 col1.palette_size times {
repeat 7 times {
reset shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
reset shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq
}
color Violet = 0x112233
palette rainbow_with_white = [
red
orange
yellow
green
blue
indigo
white
]
shutter_left_right(rainbow_with_white, 1.5s)

View File

@ -0,0 +1,32 @@
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors
set duration = 3s
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=PALETTE_RAINBOW, cycle_period=0)
color col2 = color_cycle(palette=PALETTE_RAINBOW, cycle_period=0)
col2.next = 1
animation shutter_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
log("foobar")
sequence shutter_run repeat forever {
log("before")
play shutter_animation for duration
log("after")
col1.next = 1
col2.next = 1
log("next")
}
run shutter_run

View File

@ -248,6 +248,7 @@ Cycles through a palette of colors with brutal switching. Inherits from `ColorPr
| `palette` | bytes | default palette | - | Palette bytes in AARRGGBB format |
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = manual only) |
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwars by `n` colors |
| `palette_size` | int | 3 | read-only | Number of colors in the palette (automatically updated when palette changes) |
**Modes**: Auto-cycle (`cycle_period > 0`) or Manual-only (`cycle_period = 0`)

View File

@ -537,7 +537,7 @@ anim.color = 0xFFFF0000
anim.pos = 5
anim.beacon_size = 3
engine.add_animation(anim)
engine.add(anim) # Unified method for animations and sequence managers
engine.start()
# Let it run for a few seconds

View File

@ -73,6 +73,8 @@ The following keywords are reserved and cannot be used as identifiers:
- `times` - Loop count specifier
- `for` - Duration specifier
- `run` - Execute animation or sequence
- `reset` - Reset value provider or animation to initial state
- `restart` - Restart value provider or animation from beginning
**Easing Keywords:**
- `linear` - Linear/triangle wave easing
@ -329,11 +331,58 @@ palette timed_colors = [
**Palette Rules:**
- **Value-based**: Positions range from 0 to 255, represent intensity/brightness levels
- **Tick-based**: Positions represent duration in arbitrary time units
- Colors can be hex values or named colors
- **Colors**: Only hex values (0xRRGGBB) or predefined color names (red, blue, green, etc.)
- **Custom colors**: Previously defined custom colors are NOT allowed in palettes
- **Dynamic palettes**: For palettes with custom colors, use user functions instead
- Entries are automatically sorted by position
- Comments are preserved
- Automatically converted to efficient VRGB bytes format
### Palette Color Restrictions
Palettes have strict color validation to ensure compile-time safety:
**✅ Allowed:**
```berry
palette valid_colors = [
(0, 0xFF0000) # Hex colors
(128, red) # Predefined color names
(255, blue) # More predefined colors
]
```
**❌ Not Allowed:**
```berry
color custom_red = 0xFF0000
palette invalid_colors = [
(0, custom_red) # ERROR: Custom colors not allowed
(128, my_color) # ERROR: Undefined color
]
```
**Alternative for Dynamic Palettes:**
For palettes that need custom or computed colors, use user functions:
```berry
# Define a user function that creates dynamic palettes
def create_custom_palette(engine, base_color, intensity)
# Create palette with custom logic
var palette_data = create_dynamic_palette_bytes(base_color, intensity)
return palette_data
end
# Register for DSL use
animation.register_user_function("custom_palette", create_custom_palette)
```
```berry
# Use in DSL
animation dynamic_anim = rich_palette(
palette=user.custom_palette(0xFF0000, 200)
cycle_period=3s
)
```
## Animation Definitions
The `animation` keyword defines instances of animation classes (subclasses of Animation):
@ -582,9 +631,15 @@ sequence cylon_eye repeat forever {
red_eye.pos = cosine_val
eye_color.next = 1
}
# Option 3: Parametric repeat count
sequence rainbow_cycle repeat palette.size times {
play animation for 1s
palette.next = 1
}
```
**Note**: Both syntaxes are functionally equivalent. The second syntax creates an outer sequence (runs once) containing an inner repeat sub-sequence.
**Note**: All syntaxes are functionally equivalent. The repeat count can be a literal number, variable, or dynamic expression that evaluates at runtime.
### Sequence Statements
@ -663,10 +718,22 @@ repeat forever { # Repeat indefinitely until parent sequence s
play animation for 1s
wait 500ms
}
repeat col1.palette_size times { # Parametric repeat count using property access
play animation for 1s
col1.next = 1
}
```
**Repeat Count Types:**
- **Literal numbers**: `repeat 5 times` - fixed repeat count
- **Variables**: `repeat count_var times` - using previously defined variables
- **Property access**: `repeat color_provider.palette_size times` - dynamic values from object properties
- **Computed expressions**: `repeat strip_length() / 2 times` - calculated repeat counts
**Repeat Behavior:**
- **Runtime Execution**: Repeats are executed at runtime, not expanded at compile time
- **Dynamic Evaluation**: Parametric repeat counts are evaluated when the sequence starts
- **Sub-sequences**: Each repeat block creates a sub-sequence that manages its own iteration state
- **Nested Repeats**: Supports nested repeats with multiplication (e.g., `repeat 3 times { repeat 2 times { ... } }` executes 6 times total)
- **Forever Loops**: `repeat forever` continues until the parent sequence is stopped
@ -716,6 +783,42 @@ sequence cylon_eye {
}
```
#### Reset and Restart Statements
Reset and restart statements allow you to reset value providers and animations to their initial state during sequence execution:
```berry
reset value_provider_name # Reset value provider to initial state
restart animation_name # Restart animation from beginning
```
**Reset Statement:**
- Resets value providers (oscillators, color cycles, etc.) to their initial state
- Calls the `start()` method on the value provider
- Useful for synchronizing oscillators or restarting color cycles
**Restart Statement:**
- Restarts animations from their beginning state
- Calls the `start()` method on the animation
- Useful for restarting complex animations or synchronizing multiple animations
**Examples:**
```berry
# Reset oscillators for synchronized movement
sequence sync_demo {
play wave_anim for 3s
reset position_osc # Reset oscillator to start position
play wave_anim for 3s
}
# Restart animations for clean transitions
sequence clean_transitions {
play comet_anim for 5s
restart comet_anim # Restart from beginning position
play comet_anim for 5s
}
```
## Templates
Templates provide a powerful way to create reusable, parameterized animation patterns. They allow you to define animation blueprints that can be instantiated with different parameters, promoting code reuse and maintainability.
@ -860,7 +963,7 @@ def pulse_effect_template(engine, base_color_, duration_, brightness_)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add_animation(pulse_)
engine.add(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)
@ -886,6 +989,22 @@ run animation_name # Run an animation
run sequence_name # Run a sequence
```
### Debug and Logging
Log debug messages during animation execution:
```berry
log("Debug message") # Log message at level 3
log("Animation started") # Useful for debugging sequences
log("Color changed to red") # Track animation state changes
```
**Log Function Behavior:**
- Accepts string literals only (no variables or expressions)
- Transpiles to Berry `log(f"message", 3)`
- Messages are logged at level 3 for debugging purposes
- Can be used anywhere in DSL code: standalone, in sequences, etc.
## Operators and Expressions
### Arithmetic Operators
@ -1170,14 +1289,16 @@ template_def = "template" identifier "{" template_body "}" ;
property_assignment = identifier "." identifier "=" expression ;
(* Sequences *)
sequence = "sequence" identifier [ "repeat" ( number "times" | "forever" ) ] "{" sequence_body "}" ;
sequence = "sequence" identifier [ "repeat" ( expression "times" | "forever" ) ] "{" sequence_body "}" ;
sequence_body = { sequence_statement } ;
sequence_statement = play_stmt | wait_stmt | repeat_stmt | sequence_assignment ;
sequence_statement = play_stmt | wait_stmt | repeat_stmt | sequence_assignment | reset_stmt | restart_stmt ;
play_stmt = "play" identifier [ "for" time_expression ] ;
wait_stmt = "wait" time_expression ;
repeat_stmt = "repeat" ( number "times" | "forever" ) "{" sequence_body "}" ;
repeat_stmt = "repeat" ( expression "times" | "forever" ) "{" sequence_body "}" ;
sequence_assignment = identifier "." identifier "=" expression ;
reset_stmt = "reset" identifier ;
restart_stmt = "restart" identifier ;
(* Templates *)
template_def = "template" identifier "{" template_body "}" ;

View File

@ -252,7 +252,7 @@ var test_ = animation.solid(engine)
test_.color = 0xFF0000FF
test_.opacity = animation.create_closure_value(engine,
def (self) return animation.get_user_function('rand_demo')(self.engine) end)
engine.add_animation(test_)
engine.add(test_)
engine.start()
```
@ -286,7 +286,7 @@ def pulse_effect(engine, color, speed)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = color
pulse_.period = speed
engine.add_animation(pulse_)
engine.add(pulse_)
engine.start_animation(pulse_)
end
@ -341,9 +341,9 @@ def comet_chase(engine, trail_color, bg_color, chase_speed)
var comet_ = animation.comet_animation(engine)
comet_.color = trail_color
comet_.speed = chase_speed
engine.add_animation(background_)
engine.add(background_)
engine.start_animation(background_)
engine.add_animation(comet_)
engine.add(comet_)
engine.start_animation(comet_)
end

View File

@ -89,9 +89,13 @@ animation sunrise = pulsating_animation(color=orange, period=3s)
animation day = solid(color=yellow)
sequence sunrise_show {
log("Starting sunrise sequence")
play night for 3s
log("Night phase complete, starting sunrise")
play sunrise for 5s
log("Sunrise complete, switching to day")
play day for 3s
log("Sunrise sequence finished")
}
run sunrise_show
```
@ -169,7 +173,23 @@ sequence demo {
run demo
```
### 11. Assignments in Repeat Blocks
### 11. Reset and Restart in Sequences
```berry
# Create oscillator and animation
set wave_osc = triangle(min_value=0, max_value=29, period=4s)
animation wave = beacon_animation(color=blue, pos=wave_osc, beacon_size=5)
sequence sync_demo {
play wave for 3s
reset wave_osc # Reset oscillator to start position
play wave for 3s # Wave starts from beginning again
restart wave # Restart animation from initial state
play wave for 3s
}
run sync_demo
```
### 12. Assignments in Repeat Blocks
```berry
set brightness = smooth(min_value=50, max_value=255, period=2s)
animation pulse = pulsating_animation(color=white, period=1s)
@ -187,7 +207,7 @@ run breathing_cycle
## User Functions in Computed Parameters
### 12. Simple User Function
### 13. Simple User Function
```berry
# Simple user function in computed parameter
animation random_base = solid(color=blue, priority=10)
@ -195,7 +215,7 @@ random_base.opacity = rand_demo()
run random_base
```
### 13. User Function with Math Operations
### 14. User Function with Math Operations
```berry
# Mix user functions with mathematical functions
animation random_bounded = solid(
@ -206,7 +226,7 @@ animation random_bounded = solid(
run random_bounded
```
### 14. User Function in Arithmetic Expression
### 15. User Function in Arithmetic Expression
```berry
# Use user function in arithmetic expressions
animation random_variation = solid(
@ -221,7 +241,7 @@ See `anim_examples/user_functions_demo.anim` for a complete working example.
## New Repeat System Examples
### 15. Runtime Repeat with Forever Loop
### 16. Runtime Repeat with Forever Loop
```berry
color red = 0xFF0000
color blue = 0x0000FF
@ -245,7 +265,7 @@ sequence cylon_effect_alt repeat forever {
run cylon_effect
```
### 16. Nested Repeats (Multiplication)
### 17. Nested Repeats (Multiplication)
```berry
color green = 0x00FF00
color yellow = 0xFFFF00
@ -265,7 +285,7 @@ sequence nested_pattern {
run nested_pattern
```
### 17. Repeat with Property Assignments
### 18. Repeat with Property Assignments
```berry
set triangle_pos = triangle(min_value=0, max_value=29, period=3s)
set cosine_pos = cosine_osc(min_value=0, max_value=29, period=3s)
@ -292,7 +312,7 @@ run dynamic_cylon
## Advanced Examples
### 18. Dynamic Position
### 19. Dynamic Position
```berry
strip length 60
@ -308,7 +328,7 @@ animation moving_pulse = beacon_animation(
run moving_pulse
```
### 19. Multi-Layer Effect
### 20. Multi-Layer Effect
```berry
# Base layer - slow breathing
set breathing = smooth(min_value=100, max_value=255, period=4s)

View File

@ -645,7 +645,7 @@ animation.register_user_function("pulse_effect", create_pulse_effect)
```berry
# Clear before adding new animations
engine.clear()
engine.add_animation(new_animation)
engine.add(new_animation)
```
2. **Limit Palette Size:**
@ -760,7 +760,7 @@ import animation
var engine = animation.create_engine(strip)
var red_anim = animation.solid(engine)
red_anim.color = 0xFFFF0000
engine.add_animation(red_anim)
engine.add(red_anim)
engine.start()
# If basic strip works but animation doesn't, check framework setup
@ -830,7 +830,7 @@ var engine = animation.create_engine(strip, true) # debug=true
var anim = animation.solid(engine)
anim.color = 0xFFFF0000
engine.add_animation(anim)
engine.add(anim)
engine.start()
```
@ -852,7 +852,7 @@ anim.color = 0xFFFF0000
print("Animation created:", anim != nil)
print("4. Adding animation...")
engine.add_animation(anim)
engine.add(anim)
print("Animation count:", engine.size())
print("5. Starting engine...")

View File

@ -252,6 +252,52 @@ end
animation.register_user_function("rainbow_sparkle", rainbow_sparkle)
```
### Dynamic Palettes
Since DSL palettes only accept hex colors and predefined color names (not custom colors), use user functions for dynamic palettes with custom colors:
```berry
def create_custom_palette(engine, base_color, variation_count, intensity)
# Create a palette with variations of the base color
var palette_bytes = bytes()
# Extract RGB components from base color
var r = (base_color >> 16) & 0xFF
var g = (base_color >> 8) & 0xFF
var b = base_color & 0xFF
# Create palette entries with color variations
for i : 0..(variation_count-1)
var position = int(i * 255 / (variation_count - 1))
var factor = intensity * i / (variation_count - 1) / 255
var new_r = int(r * factor)
var new_g = int(g * factor)
var new_b = int(b * factor)
# Add VRGB entry (Value, Red, Green, Blue)
palette_bytes.add(position, 1) # Position
palette_bytes.add(new_r, 1) # Red
palette_bytes.add(new_g, 1) # Green
palette_bytes.add(new_b, 1) # Blue
end
return palette_bytes
end
animation.register_user_function("custom_palette", create_custom_palette)
```
```berry
# Use dynamic palette in DSL
animation gradient_effect = rich_palette(
palette=user.custom_palette(0xFF6B35, 5, 255)
cycle_period=4s
)
run gradient_effect
```
### Preset Configurations
```berry

View File

@ -34,7 +34,6 @@ class Animation : animation.parameterized_object
# Initialize non-parameter instance variables
self.start_time = 0
self.current_time = 0
self.opacity_frame = nil # Will be created when needed
end
# Start/restart the animation (make it active and reset timing)
@ -44,7 +43,7 @@ class Animation : animation.parameterized_object
def start(start_time)
# Set is_running directly in values map to avoid infinite loop
self.values["is_running"] = true
var actual_start_time = start_time != nil ? start_time : self.engine.time_ms
var actual_start_time = (start_time != nil) ? start_time : self.engine.time_ms
self.start_time = actual_start_time
self.current_time = self.start_time
@ -63,11 +62,7 @@ class Animation : animation.parameterized_object
# Check if the parameter value is a value provider
if animation.is_value_provider(param_value)
# Call start method if it exists (acts as restart)
try
param_value.start(time_ms)
except .. as e
# Ignore errors if start method doesn't exist or fails
end
param_value.start(time_ms)
end
end
end
@ -85,7 +80,7 @@ class Animation : animation.parameterized_object
self.current_time = self.start_time
# Start/restart all value providers in parameters
self._start_value_providers(actual_start_time)
elif value == false
# elif value == false
# Stop the animation - just set the internal state
# (is_running is already set to false by the parameter system)
end
@ -143,9 +138,7 @@ class Animation : animation.parameterized_object
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
time_ms = (time_ms != nil) ? time_ms : self.engine.time_ms
# Update animation state
self.update(time_ms)
@ -226,28 +219,6 @@ class Animation : animation.parameterized_object
return self.get_color_at(0, time_ms)
end
# Get the normalized progress of the animation (0 to 255)
#
# @return int - Progress from 0 (start) to 255 (end)
def get_progress()
var current_duration = self.duration
if current_duration <= 0
return 0 # Infinite animations always return 0 progress
end
var elapsed = self.current_time - self.start_time
var progress = elapsed % current_duration # Handle looping
# For non-looping animations, if we've reached exactly the duration,
# return maximum progress instead of 0 (which would be the modulo result)
var current_loop = self.loop
if !current_loop && elapsed >= current_duration
return 255
end
return tasmota.scale_uint(progress, 0, current_duration, 0, 255)
end
# 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})"

View File

@ -43,17 +43,19 @@ class AnimationEngine
end
# Start the animation engine
#
# @return self for method chaining
def start()
if !self.is_running
var now = tasmota.millis()
self.is_running = true
self.last_update = tasmota.millis() - 10
self.last_update = now - 10
if self.fast_loop_closure == nil
self.fast_loop_closure = / -> self.on_tick()
end
var i = 0
var now = tasmota.millis()
while (i < size(self.animations))
self.animations[i].start(now)
i += 1
@ -71,6 +73,8 @@ class AnimationEngine
end
# Stop the animation engine
#
# @return self for method chaining
def stop()
if self.is_running
self.is_running = false
@ -83,25 +87,23 @@ class AnimationEngine
end
# Add an animation with automatic priority sorting
#
# @param anim: animation - The animation instance to add (if not already listed)
# @return true if succesful (TODO always true)
def add_animation(anim)
# Check if animation already exists
var i = 0
while i < size(self.animations)
if self.animations[i] == anim
return false
if (self.animations.find(anim) == nil) # not already in list
# Add and sort by priority (higher priority first)
self.animations.push(anim)
self._sort_animations()
# If the engine is already started, auto-start the animation
if self.is_running
anim.start(self.time_ms)
end
i += 1
self.render_needed = true
return true
else
return false
end
# Add and sort by priority (higher priority first)
self.animations.push(anim)
self._sort_animations()
# If the engine is already started, auto-start the animation
if self.is_running
anim.start()
end
self.render_needed = true
return true
end
# Remove an animation
@ -143,6 +145,27 @@ class AnimationEngine
return self
end
# Unified method to add either animations or sequence managers
# Detects the class type and calls the appropriate method
#
# @param obj: Animation or SequenceManager - The object to add
# @return self for method chaining
def add(obj)
# Check if it's a SequenceManager
if isinstance(obj, animation.SequenceManager)
return self.add_sequence_manager(obj)
# Check if it's an Animation (or subclass)
elif isinstance(obj, animation.animation)
self.add_animation(obj)
return self
else
# Unknown type - provide helpful error message
import introspect
var class_name = introspect.name(obj)
raise "type_error", f"Cannot add object of type '{class_name}' to engine. Expected Animation or SequenceManager."
end
end
# Remove a sequence manager
def remove_sequence_manager(sequence_manager)
var index = -1

View File

@ -13,11 +13,6 @@
# The class is optimized for performance and minimal memory usage.
class FrameBuffer
# Blend modes
# Currently only normal blending is implemented, but the structure allows for adding more modes later
static BLEND_MODE_NORMAL = 0 # Normal alpha blending
# Other blend modes can be added here in the future if needed
var pixels # Pixel data (bytes object)
var width # Number of pixels
@ -125,16 +120,11 @@ class FrameBuffer
end
end
# Blend two colors using their alpha channels and blend mode
# Blend two colors using their alpha channels
# Returns the blended color as a 32-bit integer (ARGB format - 0xAARRGGBB)
# color1: destination color (ARGB format - 0xAARRGGBB)
# color2: source color (ARGB format - 0xAARRGGBB)
# mode: blending mode (default: BLEND_MODE_NORMAL)
def blend(color1, color2, mode)
# Default blend mode to normal if not specified
if mode == nil
mode = self.BLEND_MODE_NORMAL
end
def blend(color1, color2)
# Extract components from color1 (ARGB format - 0xAARRGGBB)
var a1 = (color1 >> 24) & 0xFF
@ -157,7 +147,7 @@ class FrameBuffer
# Use the source alpha directly for blending
var effective_opacity = a2
# Normal alpha blending (currently the only supported mode)
# Normal alpha blending
# Use tasmota.scale_uint for ratio conversion instead of integer arithmetic
var r = tasmota.scale_uint(255 - effective_opacity, 0, 255, 0, r1) + tasmota.scale_uint(effective_opacity, 0, 255, 0, r2)
var g = tasmota.scale_uint(255 - effective_opacity, 0, 255, 0, g1) + tasmota.scale_uint(effective_opacity, 0, 255, 0, g2)
@ -218,16 +208,11 @@ class FrameBuffer
# Blend this frame buffer with another frame buffer using per-pixel alpha
# other_buffer: the other frame buffer to blend with
# mode: blending mode (default: BLEND_MODE_NORMAL)
# region_start: start index for blending (default: 0)
# region_end: end index for blending (default: width-1)
def blend_pixels(other_buffer, mode, region_start, region_end)
def blend_pixels(other_buffer, region_start, region_end)
# Default parameters
if mode == nil
mode = self.BLEND_MODE_NORMAL
end
if region_start == nil
region_start = 0
end
@ -249,43 +234,23 @@ class FrameBuffer
raise "index_error", "region_end out of range"
end
# Performance optimization: batch processing for normal blend mode
if mode == self.BLEND_MODE_NORMAL
# Fast path for normal blending
var i = region_start
while i <= region_end
var color2 = other_buffer.get_pixel_color(i)
var a2 = (color2 >> 24) & 0xFF
# Only blend if the source pixel has some alpha
if a2 > 0
if a2 == 255
# Fully opaque source pixel, just copy it
self.pixels.set(i * 4, color2, 4)
else
# Partially transparent source pixel, need to blend
var color1 = self.get_pixel_color(i)
var blended = self.blend(color1, color2, mode)
self.pixels.set(i * 4, blended, 4)
end
end
i += 1
end
return
end
# General case: blend each pixel using the blend function
# Blend each pixel using the blend function
var i = region_start
while i <= region_end
var color1 = self.get_pixel_color(i)
var color2 = other_buffer.get_pixel_color(i)
var a2 = (color2 >> 24) & 0xFF
# Only blend if the source pixel has some alpha
var a2 = (color2 >> 24) & 0xFF
if a2 > 0
var blended = self.blend(color1, color2, mode)
self.pixels.set(i * 4, blended, 4)
if a2 == 255
# Fully opaque source pixel, just copy it
self.pixels.set(i * 4, color2, 4)
else
# Partially transparent source pixel, need to blend
var color1 = self.get_pixel_color(i)
var blended = self.blend(color1, color2)
self.pixels.set(i * 4, blended, 4)
end
end
i += 1
@ -437,14 +402,9 @@ class FrameBuffer
# Blend a specific region with a solid color using the color's alpha channel
# color: the color to blend (ARGB)
# mode: blending mode (default: BLEND_MODE_NORMAL)
# start_pos: start position (default: 0)
# end_pos: end position (default: width-1)
def blend_color(color, mode, start_pos, end_pos)
if mode == nil
mode = self.BLEND_MODE_NORMAL
end
def blend_color(color, start_pos, end_pos)
if start_pos == nil
start_pos = 0
@ -476,7 +436,7 @@ class FrameBuffer
# Only blend if the color has some alpha
if a2 > 0
var blended = self.blend(color1, color, mode)
var blended = self.blend(color1, color)
self.pixels.set(i * 4, blended, 4)
end

View File

@ -338,34 +338,6 @@ class ParameterizedObject
return self._get_param_def(name)
end
# Get all parameter metadata from class hierarchy
#
# @return map - Map of all parameter metadata
def get_params_metadata()
import introspect
var all_params = {}
# Walk up the class hierarchy to collect all parameter definitions
var current_class = classof(self)
while current_class != nil
# Check if this class has PARAMS
if introspect.contains(current_class, "PARAMS")
var class_params = current_class.PARAMS
# Add parameters from this class (child class parameters override parent)
for param_name : class_params.keys()
if !all_params.contains(param_name) # Don't override child class params
all_params[param_name] = class_params[param_name]
end
end
end
# Move to parent class
current_class = super(current_class)
end
return all_params
end
# Helper method to get a resolved value from either a static value or a value provider
# This is the same as accessing obj.param_name but with explicit time
#

View File

@ -26,7 +26,7 @@ class SequenceManager
self.is_running = false
# Repeat logic
self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once
self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once (can be function or number)
self.current_iteration = 0
self.is_repeat_sequence = repeat_count != nil && repeat_count != 1
end
@ -56,10 +56,10 @@ class SequenceManager
return self
end
# Add an assignment step directly
def push_assign_step(closure)
# Add a closure step directly (used for both assign and log steps)
def push_closure_step(closure)
self.steps.push({
"type": "assign",
"type": "closure",
"closure": closure
})
return self
@ -150,7 +150,7 @@ class SequenceManager
# Sub-sequence finished, advance to next step
self.advance_to_next_step(current_time)
end
elif current_step["type"] == "assign"
elif current_step["type"] == "closure"
# Assign steps are handled in batches by advance_to_next_step
# This should not happen in normal flow, but handle it just in case
self.execute_assign_steps_batch(current_time)
@ -192,18 +192,18 @@ class SequenceManager
var anim = step["animation"]
self.engine.remove_animation(anim)
elif step["type"] == "assign"
# Assign steps should be handled in batches by execute_assign_steps_batch
elif step["type"] == "closure"
# Closure steps should be handled in batches by execute_assign_steps_batch
# This should not happen in normal flow, but handle it for safety
var assign_closure = step["closure"]
if assign_closure != nil
assign_closure(self.engine)
var closure_func = step["closure"]
if closure_func != nil
closure_func(self.engine)
end
elif step["type"] == "subsequence"
# Start sub-sequence (including repeat sequences)
var sub_seq = step["sequence_manager"]
sub_seq.start()
sub_seq.start(current_time)
end
self.step_start_time = current_time
@ -228,16 +228,16 @@ class SequenceManager
end
end
# Execute all consecutive assign steps in a batch to avoid black frames
# Execute all consecutive closure steps in a batch to avoid black frames
def execute_assign_steps_batch(current_time)
# Execute all consecutive assign steps
# Execute all consecutive closure steps (including both assign and log steps)
while self.step_index < size(self.steps)
var step = self.steps[self.step_index]
if step["type"] == "assign"
# Execute assignment closure
var assign_closure = step["closure"]
if assign_closure != nil
assign_closure(self.engine)
if step["type"] == "closure"
# Execute closure function
var closure_func = step["closure"]
if closure_func != nil
closure_func(self.engine)
end
self.step_index += 1
else
@ -257,8 +257,11 @@ class SequenceManager
def complete_iteration(current_time)
self.current_iteration += 1
# Resolve repeat count (may be a function)
var resolved_repeat_count = self.get_resolved_repeat_count()
# Check if we should continue repeating
if self.repeat_count == -1 || self.current_iteration < self.repeat_count
if resolved_repeat_count == -1 || self.current_iteration < resolved_repeat_count
# Start next iteration
self.step_index = 0
self.execute_current_step(current_time)
@ -268,6 +271,15 @@ class SequenceManager
end
end
# Resolve repeat count (handle both functions and numbers)
def get_resolved_repeat_count()
if type(self.repeat_count) == "function"
return self.repeat_count(self.engine)
else
return self.repeat_count
end
end
# Check if sequence is running
def is_sequence_running()
return self.is_running

View File

@ -40,7 +40,7 @@ class Token
# Control flow keywords
"play", "for", "with", "repeat", "times", "forever", "if", "else", "elif",
"choose", "random", "on", "run", "wait", "goto", "interrupt", "resume",
"while", "from", "to", "return",
"while", "from", "to", "return", "reset", "restart",
# Modifier keywords (only actual DSL syntax keywords)
"at", "ease", "sync", "every", "stagger", "across", "pixels",

View File

@ -120,7 +120,7 @@ class SimpleDSLTranspiler
var obj_name = run_stmt["name"]
var comment = run_stmt["comment"]
# In templates, use underscore suffix for local variables
self.add(f"engine.add_animation({obj_name}_){comment}")
self.add(f"engine.add({obj_name}_){comment}")
end
end
@ -197,8 +197,14 @@ class SimpleDSLTranspiler
if !self.strip_initialized
self.generate_default_strip_initialization()
end
# Check if this is a property assignment (identifier.property = value)
self.process_property_assignment()
# Check if this is a log function call
if tok.value == "log" && self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
self.process_standalone_log()
else
# Check if this is a property assignment (identifier.property = value)
self.process_property_assignment()
end
else
self.skip_statement()
end
@ -280,6 +286,9 @@ class SimpleDSLTranspiler
if type(ref_instance) != "string"
self.symbol_table[name] = ref_instance
end
else
# Add simple color to symbol table with a marker
self.symbol_table[name] = "color"
end
end
end
@ -333,7 +342,7 @@ class SimpleDSLTranspiler
self.expect_left_paren()
var value = self.expect_number()
self.expect_comma()
var color = self.process_value("color") # Reuse existing color parsing
var color = self.process_palette_color() # Use specialized palette color processing
self.expect_right_paren()
# Convert to VRGB format entry and store as integer
@ -349,7 +358,7 @@ class SimpleDSLTranspiler
return
end
var color = self.process_value("color") # Reuse existing color parsing
var color = self.process_palette_color() # Use specialized palette color processing
# Convert to VRGB format entry and store as integer after setting alpha to 0xFF
var vrgb_entry = self.convert_to_vrgb(0xFF, color)
@ -474,6 +483,9 @@ class SimpleDSLTranspiler
if type(ref_instance) != "string"
self.symbol_table[name] = ref_instance
end
else
# Add simple animation to symbol table with a marker
self.symbol_table[name] = "animation"
end
end
end
@ -503,13 +515,31 @@ class SimpleDSLTranspiler
end
self.expect_assign()
# Check if this is a value provider function call
var is_value_provider = false
var tok = self.current()
if (tok.type == animation_dsl.Token.KEYWORD || tok.type == animation_dsl.Token.IDENTIFIER) &&
self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
var func_name = tok.value
# Check if this is a value provider factory function
if self._validate_value_provider_factory_exists(func_name)
is_value_provider = true
end
end
var value = self.process_value("variable")
var inline_comment = self.collect_inline_comment()
self.add(f"var {name}_ = {value}{inline_comment}")
# Add variable to symbol table for validation
# Use a string marker to indicate this is a variable (not an instance)
self.symbol_table[name] = "variable"
# Add to symbol table with appropriate marker
if is_value_provider
self.symbol_table[name] = "value_provider"
else
# Use a string marker to indicate this is a variable (not an instance)
self.symbol_table[name] = "variable"
end
end
# Process template definition: template name { param ... }
@ -569,7 +599,7 @@ class SimpleDSLTranspiler
var body_tokens = []
var brace_depth = 0
while !self.at_end() && !self.check_right_brace()
while !self.at_end()
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
@ -643,9 +673,9 @@ class SimpleDSLTranspiler
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
else
var count = self.expect_number()
var count_expr = self.process_value("repeat_count")
self.expect_keyword("times")
repeat_count = str(count)
repeat_count = count_expr
end
elif current_tok.value == "forever"
# New syntax: sequence name forever { ... } (repeat is optional)
@ -656,9 +686,9 @@ class SimpleDSLTranspiler
elif current_tok != nil && current_tok.type == animation_dsl.Token.NUMBER
# New syntax: sequence name N times { ... } (repeat is optional)
is_repeat_syntax = true
var count = self.expect_number()
var count_expr = self.process_value("repeat_count")
self.expect_keyword("times")
repeat_count = str(count)
repeat_count = count_expr
end
self.expect_left_brace()
@ -712,6 +742,12 @@ class SimpleDSLTranspiler
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "wait"
self.process_wait_statement_fluent()
elif tok.type == animation_dsl.Token.IDENTIFIER && tok.value == "log"
self.process_log_statement_fluent()
elif tok.type == animation_dsl.Token.KEYWORD && (tok.value == "reset" || tok.value == "restart")
self.process_reset_restart_statement_fluent()
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "repeat"
self.next() # skip 'repeat'
@ -722,9 +758,9 @@ class SimpleDSLTranspiler
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
else
var count = self.expect_number()
var count_expr = self.process_value("repeat_count")
self.expect_keyword("times")
repeat_count = str(count)
repeat_count = count_expr
end
self.expect_left_brace()
@ -751,9 +787,13 @@ class SimpleDSLTranspiler
if self.peek() != nil && self.peek().type == animation_dsl.Token.DOT
self.process_sequence_assignment_fluent()
else
# Unknown identifier in sequence - this is an error
self.error(f"Unknown command '{tok.value}' in sequence. Valid sequence commands are: play, wait, repeat, reset, restart, log, or property assignments (object.property = value)")
self.skip_statement()
end
else
# Unknown token type in sequence - this is an error
self.error(f"Invalid statement in sequence. Expected: play, wait, repeat, reset, restart, log, or property assignments")
self.skip_statement()
end
end
@ -769,7 +809,7 @@ class SimpleDSLTranspiler
# Create assignment step using fluent style
var closure_code = f"def (engine) {object_name}_.{property_name} = {value} end"
self.add(f"{self.get_indent()}.push_assign_step({closure_code}){inline_comment}")
self.add(f"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}")
end
# Process property assignment inside sequences: object.property = value (legacy)
@ -870,7 +910,66 @@ class SimpleDSLTranspiler
var inline_comment = self.collect_inline_comment()
self.add(f"{self.get_indent()}.push_wait_step({duration}){inline_comment}")
end
# Unified log processing method - handles all log contexts
def process_log_call(args_str, context_type, inline_comment)
# Convert DSL log("message") to Berry log(f"message", 3)
if context_type == "fluent"
# For sequence context - wrap in closure
var closure_code = f"def (engine) log(f\"{args_str}\", 3) end"
return f"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}"
elif context_type == "expression"
# For expression context - return just the call (no inline comment)
return f"log(f\"{args_str}\", 3)"
else
# For standalone context - direct call with comment
return f"log(f\"{args_str}\", 3){inline_comment}"
end
end
# Helper method to process log statement using fluent style
def process_log_statement_fluent()
self.next() # skip 'log'
self.expect_left_paren()
# Process the message string
var message_tok = self.current()
if message_tok == nil || message_tok.type != animation_dsl.Token.STRING
self.error("log() function requires a string message")
self.skip_statement()
return
end
var message = message_tok.value
self.next() # consume string
self.expect_right_paren()
var inline_comment = self.collect_inline_comment()
# Use unified log processing
var log_code = self.process_log_call(message, "fluent", inline_comment)
self.add(log_code)
end
# Helper method to process reset/restart statement using fluent style
def process_reset_restart_statement_fluent()
var keyword = self.current().value # "reset" or "restart"
self.next() # skip 'reset' or 'restart'
# Expect the value provider identifier
var val_name = self.expect_identifier()
# Validate that the value is a value_provider at transpile time
if !self._validate_value_provider_reference(val_name, keyword)
self.skip_statement()
return
end
var inline_comment = self.collect_inline_comment()
# Generate closure step that calls val.start(engine.time_ms)
var closure_code = f"def (engine) {val_name}_.start(engine.time_ms) end"
self.add(f"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}")
end
# Process import statement: import user_functions or import module_name
def process_import()
@ -883,6 +982,29 @@ class SimpleDSLTranspiler
self.add(f'import {module_name} {inline_comment}')
end
# Process standalone log statement: log("message")
def process_standalone_log()
self.next() # skip 'log'
self.expect_left_paren()
# Process the message string
var message_tok = self.current()
if message_tok == nil || message_tok.type != animation_dsl.Token.STRING
self.error("log() function requires a string message")
self.skip_statement()
return
end
var message = message_tok.value
self.next() # consume string
self.expect_right_paren()
var inline_comment = self.collect_inline_comment()
# Use unified log processing
var log_code = self.process_log_call(message, "standalone", inline_comment)
self.add(log_code)
end
# Process run statement: run demo
def process_run()
self.next() # skip 'run'
@ -904,8 +1026,18 @@ class SimpleDSLTranspiler
def process_property_assignment()
var object_name = self.expect_identifier()
# Check if this is a function call (template call)
# Check if this is a function call (template call or special function)
if self.current() != nil && self.current().type == animation_dsl.Token.LEFT_PAREN
# Special case for log function - allow as standalone
if object_name == "log"
var args = self.process_function_arguments()
var inline_comment = self.collect_inline_comment()
# Use unified log processing
var log_code = self.process_log_call(args, "standalone", inline_comment)
self.add(log_code)
return
end
# This is a standalone function call - check if it's a template
if self.template_definitions.contains(object_name)
var args = self.process_function_arguments()
@ -972,6 +1104,40 @@ class SimpleDSLTranspiler
return self.process_additive_expression(context, true) # true = top-level expression
end
# Process palette color with strict validation
# Only accepts predefined color names or hex color literals
def process_palette_color()
var tok = self.current()
if tok == nil
self.error("Expected color value in palette")
return "0xFFFFFFFF"
end
# Handle hex color literals
if tok.type == animation_dsl.Token.COLOR
self.next()
return self.convert_color(tok.value)
end
# Handle identifiers (color names)
if tok.type == animation_dsl.Token.IDENTIFIER
var name = tok.value
self.next()
# Only accept predefined color names
if animation_dsl.is_color_name(name)
return self.get_named_color_value(name)
end
# Reject any other identifier
self.error(f"Unknown color '{name}'. Palettes only accept hex colors (0xRRGGBB) or predefined color names (like 'red', 'blue', 'green'), but not custom colors defined previously. For dynamic palettes with custom colors, use user functions instead.")
return "0xFFFFFFFF"
end
self.error("Expected color value in palette. Use hex colors (0xRRGGBB) or predefined color names (like 'red', 'blue', 'green').")
return "0xFFFFFFFF"
end
# Process additive expressions (+ and -)
def process_additive_expression(context, is_top_level)
var left = self.process_multiplicative_expression(context, is_top_level)
@ -989,8 +1155,20 @@ class SimpleDSLTranspiler
end
# Only create closures at the top level, but not for anonymous functions
if is_top_level && self.is_computed_expression_string(left) && !self.is_anonymous_function(left)
return self.create_computation_closure_from_string(left)
if is_top_level && !self.is_anonymous_function(left)
# Special handling for repeat_count context - always create simple function for property access
if context == "repeat_count"
import string
if self.is_computed_expression_string(left) || string.find(left, ".") >= 0
return self.create_simple_function_from_string(left)
else
return left
end
elif self.is_computed_expression_string(left)
return self.create_computation_closure_from_string(left)
else
return left
end
else
return left
end
@ -1147,8 +1325,13 @@ class SimpleDSLTranspiler
object_ref = f"{name}_"
end
# Return a closure expression that will be wrapped by the caller if needed
return f"self.resolve({object_ref}, '{property_name}')"
# For repeat_count context, generate simple property access
if context == "repeat_count"
return f"{object_ref}.{property_name}"
else
# Return a closure expression that will be wrapped by the caller if needed
return f"self.resolve({object_ref}, '{property_name}')"
end
end
# Check for palette constants
@ -1253,6 +1436,8 @@ class SimpleDSLTranspiler
return has_dynamic_content
end
# Create a closure for computed expressions from a complete expression string
def create_computation_closure_from_string(expr_str)
import string
@ -1296,6 +1481,13 @@ class SimpleDSLTranspiler
return f"animation.create_closure_value(engine, {closure_code})"
end
# Create a simple function for repeat counts (no closure wrapper)
def create_simple_function_from_string(expr_str)
# For repeat counts, create a simple function that takes engine and returns the value
# The expression should already be in simple form like "col1_.palette_size"
return f"def (engine) return {expr_str} end"
end
# Transform a complete expression for use in a closure, handling ValueProvider instances
def transform_expression_for_closure(expr_str)
import string
@ -1489,6 +1681,13 @@ class SimpleDSLTranspiler
return f"{func_name}({args})" # Return as-is for transformation in closure
end
# Special case for log function - call global log function directly
if func_name == "log"
var args = self.process_function_arguments()
# Use unified log processing (return expression for use in contexts)
return self.process_log_call(args, "expression", "")
end
var args = self.process_function_arguments()
# Check if it's a template call first
@ -1811,6 +2010,13 @@ class SimpleDSLTranspiler
return f"self.{func_name}({args})"
end
# Special case for log function in expressions
if func_name == "log"
var args = self.process_function_arguments_for_expression()
# Use unified log processing
return self.process_log_call(args, "expression", "")
end
# Check if this is a template call
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
@ -1927,6 +2133,13 @@ class SimpleDSLTranspiler
return f"self.{func_name}({args})" # Prefix with self. for closure context
end
# Special case for log function in nested calls
if func_name == "log"
var args = self.process_function_arguments_for_expression()
# Use unified log processing
return self.process_log_call(args, "expression", "")
end
# Check if this is a template call
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
@ -2382,13 +2595,8 @@ class SimpleDSLTranspiler
var comment = run_stmt["comment"]
# Check if this is a sequence or regular animation
if self.sequence_names.contains(name)
# It's a sequence - the closure returned a SequenceManager
self.add(f"engine.add_sequence_manager({name}_){comment}")
else
# It's a regular animation
self.add(f"engine.add_animation({name}_){comment}")
end
# Use unified add() method - it will detect the type automatically
self.add(f"engine.add({name}_){comment}")
end
# Single engine.start() call
@ -2430,7 +2638,7 @@ class SimpleDSLTranspiler
# Assume it's an animation function call or reference
var action = self.process_value("animation")
self.add(f" var temp_anim = {action}")
self.add(f" engine.add_animation(temp_anim)")
self.add(f" engine.add(temp_anim)")
end
end
@ -2713,6 +2921,47 @@ class SimpleDSLTranspiler
return self._validate_factory_function(func_name, animation.color_provider)
end
# Validate value provider factory exists and creates animation.value_provider instance
def _validate_value_provider_factory_exists(func_name)
return self._validate_factory_function(func_name, animation.value_provider)
end
# Validate that a referenced object is a value provider or animation
def _validate_value_provider_reference(object_name, context)
try
# Check if object exists in symbol table (user-defined)
if self.symbol_table.contains(object_name)
var marker = self.symbol_table[object_name]
# Check if it's marked as a value provider or animation
if type(marker) == "string" && (marker == "value_provider" || marker == "animation")
return true # Valid value provider or animation
elif type(marker) == "string"
# It's some other type (variable, color, sequence, etc.)
self.error(f"'{object_name}' in {context} statement is not a value provider or animation. Only value providers (like oscillators) and animations can be reset/restarted.")
return false
else
# It's an actual instance - check if it's a value provider or animation
if isinstance(marker, animation.value_provider) || isinstance(marker, animation.animation)
return true # Valid value provider or animation
else
self.error(f"'{object_name}' in {context} statement is not a value provider or animation. Only value providers (like oscillators) and animations can be reset/restarted.")
return false
end
end
end
# Object not found in symbol table
self.error(f"Undefined reference '{object_name}' in {context} statement. Make sure the value provider or animation is defined before use.")
return false
except .. as e, msg
# If validation fails for any reason, report error but continue
self.error(f"Could not validate '{object_name}' in {context} statement: {msg}")
return false
end
end
# Process named arguments with parameter validation at transpile time
def _process_named_arguments_generic(var_name, func_name)
self.expect_left_paren()
@ -2777,6 +3026,7 @@ class SimpleDSLTranspiler
# Check if this is a simple function call that doesn't need anonymous function treatment
def _is_simple_function_call(func_name)
# Functions that return simple values and don't use named parameters
# Note: log is handled by unified process_log_call method
var simple_functions = [
"strip_length",
"static_value"

View File

@ -27,7 +27,8 @@ class ColorCycleColorProvider : animation.color_provider
)
},
"cycle_period": {"min": 0, "default": 5000}, # 0 = manual only, >0 = auto cycle time in ms
"next": {"default": 0} # Write 1 to move to next color
"next": {"default": 0}, # Write `<n>` to move to next <n> colors
"palette_size": {"type": "int", "default": 3} # Read-only: number of colors in palette
}
# Initialize a new ColorCycleColorProvider
@ -40,6 +41,9 @@ class ColorCycleColorProvider : animation.color_provider
var palette_bytes = self._get_palette_bytes()
self.current_color = self._get_color_at_index(0) # Start with first color in palette
self.current_index = 0 # Start at first color
# Initialize palette_size parameter
self.values["palette_size"] = self._get_palette_size()
end
# Get palette bytes from parameter with default fallback
@ -82,7 +86,11 @@ class ColorCycleColorProvider : animation.color_provider
# @param name: string - Name of the parameter that changed
# @param value: any - New value of the parameter
def on_param_changed(name, value)
if name == "palette"
if name == "palette_size"
# palette_size is read-only - restore the actual value and raise an exception
self.values["palette_size"] = self._get_palette_size()
raise "value_error", "Parameter 'palette_size' is read-only"
elif name == "palette"
# When palette changes, update current_color if current_index is valid
var palette_size = self._get_palette_size()
if palette_size > 0
@ -92,6 +100,8 @@ class ColorCycleColorProvider : animation.color_provider
end
self.current_color = self._get_color_at_index(self.current_index)
end
# Update palette_size parameter
self.values["palette_size"] = palette_size
elif name == "next" && value != 0
# Add to color index
var palette_size = self._get_palette_size()

View File

@ -49,6 +49,7 @@ opacity_engine.start()
var opacity_frame = animation.frame_buffer(10)
base_anim.start()
var render_result = base_anim.render(opacity_frame, opacity_engine.time_ms)
base_anim.post_render(opacity_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation with numeric opacity should render successfully")
assert_equals(base_anim.opacity, 128, "Numeric opacity should be preserved")
@ -86,6 +87,7 @@ opacity_mask.start()
# Test rendering with animation opacity
var masked_frame = animation.frame_buffer(10)
render_result = masked_anim.render(masked_frame, opacity_engine.time_ms)
masked_anim.post_render(masked_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation with animation opacity should render successfully")
assert_not_nil(masked_anim.opacity_frame, "Opacity frame buffer should be created")
@ -122,6 +124,7 @@ var base_time = 2000
pulsing_opacity.color = 0xFF808080 # Gray (50% opacity)
render_result = rainbow_base.render(test_frame, base_time)
rainbow_base.post_render(test_frame, base_time)
assert_test(render_result, "Complex opacity animation should render successfully")
# Test 11f: Opacity animation lifecycle management
@ -147,6 +150,7 @@ assert_test(!auto_start_opacity.is_running, "Opacity animation should start stop
auto_start_main.start()
var auto_frame = animation.frame_buffer(10)
render_result = auto_start_main.render(auto_frame, opacity_engine.time_ms)
auto_start_main.post_render(auto_frame, opacity_engine.time_ms)
# Opacity animation should now be running
assert_test(auto_start_opacity.is_running, "Opacity animation should auto-start when main animation renders")
@ -185,6 +189,8 @@ opacity2.start()
var nested_frame = animation.frame_buffer(10)
render_result = base_nested.render(nested_frame, opacity_engine.time_ms)
base_nested.post_render(nested_frame, opacity_engine.time_ms)
opacity1.post_render(nested_frame, opacity_engine.time_ms)
assert_test(render_result, "Nested animation opacity should render successfully")
assert_not_nil(base_nested.opacity_frame, "Base animation should have opacity frame buffer")
@ -211,21 +217,25 @@ param_opacity.start()
var param_frame = animation.frame_buffer(10)
render_result = param_base.render(param_frame, opacity_engine.time_ms)
param_base.post_render(param_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation should render before opacity parameter change")
# Change opacity animation color
param_opacity.color = 0xFFFFFFFF # White (full opacity)
render_result = param_base.render(param_frame, opacity_engine.time_ms + 100)
param_base.post_render(param_frame, opacity_engine.time_ms + 100)
assert_test(render_result, "Animation should render after opacity parameter change")
# Change opacity animation to numeric value
param_base.opacity = 64 # 25% opacity
render_result = param_base.render(param_frame, opacity_engine.time_ms + 200)
param_base.post_render(param_frame, opacity_engine.time_ms + 200)
assert_test(render_result, "Animation should render after changing from animation to numeric opacity")
# Change back to animation opacity
param_base.opacity = param_opacity
render_result = param_base.render(param_frame, opacity_engine.time_ms + 300)
param_base.post_render(param_frame, opacity_engine.time_ms + 300)
assert_test(render_result, "Animation should render after changing from numeric to animation opacity")
# Test 11i: Opacity with full transparency and full opacity
@ -241,11 +251,13 @@ edge_base.opacity = 0
edge_base.start()
var edge_frame = animation.frame_buffer(10)
render_result = edge_base.render(edge_frame, opacity_engine.time_ms)
edge_base.post_render(edge_frame, opacity_engine.time_ms)
assert_test(render_result, "Animation with 0 opacity should still render")
# Test full opacity (should render normally)
edge_base.opacity = 255
render_result = edge_base.render(edge_frame, opacity_engine.time_ms + 100)
edge_base.post_render(edge_frame, opacity_engine.time_ms + 100)
assert_test(render_result, "Animation with full opacity should render normally")
# Test transparent animation as opacity
@ -257,6 +269,7 @@ transparent_opacity.name = "transparent_opacity"
edge_base.opacity = transparent_opacity
transparent_opacity.start()
render_result = edge_base.render(edge_frame, opacity_engine.time_ms + 200)
edge_base.post_render(edge_frame, opacity_engine.time_ms + 200)
assert_test(render_result, "Animation with transparent animation opacity should render")
# Test 11j: Performance with animation opacity

View File

@ -104,82 +104,6 @@ assert(result == true, "Update should return true for looping animation")
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")
# Test get_progress
engine.time_ms = 4500
var non_loop_progress = animation.animation(engine)
non_loop_progress.priority = 1
non_loop_progress.duration = 1000
non_loop_progress.loop = false
non_loop_progress.opacity = 255
non_loop_progress.name = "progress"
non_loop_progress.color = 0xFF0000
non_loop_progress.start(4000)
non_loop_progress.update(engine.time_ms)
assert(non_loop_progress.get_progress() == 128, "Progress should be 128 at midpoint (500ms of 1000ms)")
# Test progress at start (0ms elapsed)
engine.time_ms = 4000
var start_progress = animation.animation(engine)
start_progress.priority = 1
start_progress.duration = 1000
start_progress.loop = false
start_progress.opacity = 255
start_progress.name = "start"
start_progress.color = 0xFF0000
start_progress.start(4000)
start_progress.update(engine.time_ms)
assert(start_progress.get_progress() == 0, "Progress should be 0 at start")
# Test progress at end (1000ms elapsed) - test before update stops the animation
engine.time_ms = 5000
var end_progress = animation.animation(engine)
end_progress.priority = 1
end_progress.duration = 1000
end_progress.loop = false
end_progress.opacity = 255
end_progress.name = "end"
end_progress.color = 0xFF0000
end_progress.start(4000)
end_progress.current_time = 5000 # Set current time manually to avoid stopping
assert(end_progress.get_progress() == 255, "Progress should be 255 at end")
# Test progress at quarter point (250ms elapsed)
engine.time_ms = 4250
var quarter_progress = animation.animation(engine)
quarter_progress.priority = 1
quarter_progress.duration = 1000
quarter_progress.loop = false
quarter_progress.opacity = 255
quarter_progress.name = "quarter"
quarter_progress.color = 0xFF0000
quarter_progress.start(4000)
quarter_progress.update(engine.time_ms)
assert(quarter_progress.get_progress() == 64, "Progress should be 64 at quarter point (250ms of 1000ms)")
# Test looping animation progress (should wrap around)
engine.time_ms = 5500 # 1500ms elapsed = 1.5 loops of 1000ms
var loop_progress = animation.animation(engine)
loop_progress.priority = 1
loop_progress.duration = 1000
loop_progress.loop = true
loop_progress.opacity = 255
loop_progress.name = "loop_progress"
loop_progress.color = 0xFF0000
loop_progress.start(4000)
loop_progress.current_time = 5500 # Set manually to avoid loop adjustment in update()
assert(loop_progress.get_progress() == 128, "Looping animation should wrap around (500ms into second loop)")
# Test infinite animation progress
var infinite_anim = animation.animation(engine)
infinite_anim.priority = 1
infinite_anim.duration = 0 # infinite
infinite_anim.loop = false
infinite_anim.opacity = 255
infinite_anim.name = "infinite"
infinite_anim.color = 0xFF0000
infinite_anim.start(4000)
assert(infinite_anim.get_progress() == 0, "Infinite animation should always return 0 progress")
# Test direct parameter assignment (no setter methods needed)
var setter_anim = animation.animation(engine)
setter_anim.priority = 20
@ -192,12 +116,6 @@ assert(setter_anim.loop == true, "Loop should be updated")
# Test parameter handling with static parameters
var param_anim = animation.animation(engine)
# Test static parameter metadata access
var params_metadata = param_anim.get_params_metadata()
assert(params_metadata.contains("priority"), "Priority parameter should be defined")
assert(params_metadata["priority"]["min"] == 0, "Priority parameter min should be 0")
assert(params_metadata["priority"]["default"] == 10, "Priority parameter default should be 10")
# Test parameter validation and setting (using existing 'priority' parameter)
assert(param_anim.set_param("priority", 75) == true, "Valid parameter should be accepted")
assert(param_anim.get_param("priority", nil) == 75, "Parameter value should be updated")

View File

@ -0,0 +1,191 @@
#!/usr/bin/env berry
# Test for ColorCycleColorProvider palette_size read-only parameter
import animation
# Mock engine for testing
class MockEngine
var time_ms
def init()
self.time_ms = 1000
end
end
def test_palette_size_parameter_access()
print("Testing palette_size parameter access...")
var engine = MockEngine()
var provider = animation.color_cycle(engine)
# Test 1: Default palette_size should be 3
var default_size = provider.palette_size
assert(default_size == 3, f"Default palette_size should be 3, got {default_size}")
# Test 2: palette_size should match _get_palette_size()
var internal_size = provider._get_palette_size()
assert(default_size == internal_size, f"palette_size ({default_size}) should match _get_palette_size() ({internal_size})")
print("✓ palette_size parameter access tests passed!")
end
def test_palette_size_read_only()
print("Testing palette_size is read-only...")
var engine = MockEngine()
var provider = animation.color_cycle(engine)
var original_size = provider.palette_size
# Test 1: Direct assignment should raise exception
var caught_exception = false
try
provider.palette_size = 10
except "value_error" as e
caught_exception = true
end
assert(caught_exception, "Direct assignment to palette_size should raise value_error")
# Test 2: Value should remain unchanged after failed write
var size_after_write = provider.palette_size
assert(size_after_write == original_size, f"palette_size should remain {original_size} after failed write, got {size_after_write}")
# Test 3: set_param method should return false and not change value
var set_success = provider.set_param("palette_size", 99)
assert(set_success == false, "set_param should return false for read-only parameter")
var size_after_set_param = provider.palette_size
assert(size_after_set_param == original_size, f"palette_size should remain {original_size} after set_param, got {size_after_set_param}")
# Test 4: get_param should return the actual value, not the attempted write
var raw_value = provider.get_param("palette_size")
assert(raw_value == original_size, f"get_param should return actual value {original_size}, got {raw_value}")
print("✓ palette_size read-only tests passed!")
end
def test_palette_size_updates_with_palette_changes()
print("Testing palette_size updates when palette changes...")
var engine = MockEngine()
var provider = animation.color_cycle(engine)
# Test 1: 2-color palette
var palette_2 = bytes("FFFF0000" "FF00FF00")
provider.palette = palette_2
var size_2 = provider.palette_size
assert(size_2 == 2, f"palette_size should be 2 for 2-color palette, got {size_2}")
# Test 2: 5-color palette
var palette_5 = bytes("FFFF0000" "FF00FF00" "FF0000FF" "FFFFFF00" "FFFF00FF")
provider.palette = palette_5
var size_5 = provider.palette_size
assert(size_5 == 5, f"palette_size should be 5 for 5-color palette, got {size_5}")
# Test 3: 1-color palette
var palette_1 = bytes("FFFF0000")
provider.palette = palette_1
var size_1 = provider.palette_size
assert(size_1 == 1, f"palette_size should be 1 for 1-color palette, got {size_1}")
# Test 4: Empty palette
var empty_palette = bytes()
provider.palette = empty_palette
var size_0 = provider.palette_size
assert(size_0 == 0, f"palette_size should be 0 for empty palette, got {size_0}")
# Test 5: Large palette (10 colors)
var palette_10 = bytes(
"FFFF0000" "FF00FF00" "FF0000FF" "FFFFFF00" "FFFF00FF"
"FF800000" "FF008000" "FF000080" "FF808000" "FF800080"
)
provider.palette = palette_10
var size_10 = provider.palette_size
assert(size_10 == 10, f"palette_size should be 10 for 10-color palette, got {size_10}")
# Test 6: Verify palette_size is still read-only after palette changes
var caught_exception = false
try
provider.palette_size = 15
except "value_error"
caught_exception = true
end
assert(caught_exception, "palette_size should still be read-only after palette changes")
var final_size = provider.palette_size
assert(final_size == 10, f"palette_size should remain 10 after failed write, got {final_size}")
print("✓ palette_size update tests passed!")
end
def test_palette_size_with_new_instances()
print("Testing palette_size with new provider instances...")
var engine = MockEngine()
# Test 1: Multiple instances should have correct default palette_size
var provider1 = animation.color_cycle(engine)
var provider2 = animation.color_cycle(engine)
assert(provider1.palette_size == 3, "First provider should have default palette_size of 3")
assert(provider2.palette_size == 3, "Second provider should have default palette_size of 3")
# Test 2: Changing one instance shouldn't affect the other
var custom_palette = bytes("FFFF0000" "FF00FF00")
provider1.palette = custom_palette
assert(provider1.palette_size == 2, "First provider should have palette_size of 2 after change")
assert(provider2.palette_size == 3, "Second provider should still have palette_size of 3")
# Test 3: Both instances should maintain read-only behavior
var caught_exception_1 = false
var caught_exception_2 = false
try
provider1.palette_size = 5
except "value_error"
caught_exception_1 = true
end
try
provider2.palette_size = 7
except "value_error"
caught_exception_2 = true
end
assert(caught_exception_1, "First provider should reject palette_size writes")
assert(caught_exception_2, "Second provider should reject palette_size writes")
assert(provider1.palette_size == 2, "First provider palette_size should remain 2")
assert(provider2.palette_size == 3, "Second provider palette_size should remain 3")
print("✓ Multiple instance tests passed!")
end
def test_palette_size_parameter_metadata()
print("Testing palette_size parameter metadata...")
var engine = MockEngine()
var provider = animation.color_cycle(engine)
# Test 1: Parameter should exist in metadata
var metadata = provider.get_param_metadata("palette_size")
assert(metadata != nil, "palette_size should have metadata")
# Test 2: Check parameter definition
assert(metadata.contains("type"), "palette_size metadata should have type")
assert(metadata["type"] == "int", f"palette_size type should be 'int', got '{metadata['type']}'")
assert(metadata.contains("default"), "palette_size metadata should have default")
assert(metadata["default"] == 3, f"palette_size default should be 3, got {metadata['default']}")
print("✓ Parameter metadata tests passed!")
end
# Run all tests
test_palette_size_parameter_access()
test_palette_size_read_only()
test_palette_size_updates_with_palette_changes()
test_palette_size_with_new_instances()
test_palette_size_parameter_metadata()
print("✓ All ColorCycleColorProvider palette_size tests completed successfully!")

View File

@ -203,7 +203,7 @@ def test_sequence_processing()
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
assert(string.find(berry_code, "red_anim") >= 0, "Should reference animation")
assert(string.find(berry_code, ".push_play_step(red_anim_, 2000)") >= 0, "Should create play step")
assert(string.find(berry_code, "engine.add_sequence_manager(demo_)") >= 0, "Should add sequence manager")
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should add sequence manager")
assert(string.find(berry_code, "engine.start()") >= 0, "Should start engine")
# Test repeat in sequence

View File

@ -0,0 +1,397 @@
# DSL Reset/Restart Test Suite
# Tests for reset and restart functionality in sequences
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota def log(x) print(x) end" lib/libesp32/berry_animation/src/tests/dsl_reset_restart_test.be
import animation
import animation_dsl
import string
# Test basic reset functionality
def test_reset_basic()
print("Testing basic reset functionality...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=2s)\n" +
"animation test_anim = solid(color=red)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset osc_val\n" +
" play test_anim for 1s\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for reset")
assert(string.find(berry_code, "animation.triangle(engine)") >= 0, "Should generate triangle oscillator")
assert(string.find(berry_code, "push_closure_step") >= 0, "Should generate closure step for reset")
assert(string.find(berry_code, "osc_val_.start(engine.time_ms)") >= 0, "Should call start() method")
print("✓ Basic reset test passed")
return true
end
# Test basic restart functionality
def test_restart_basic()
print("Testing basic restart functionality...")
var dsl_source = "set smooth_val = smooth(min_value=5, max_value=15, duration=3s)\n" +
"animation test_anim = solid(color=blue)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" restart smooth_val\n" +
" play test_anim for 1s\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for restart")
assert(string.find(berry_code, "animation.smooth(engine)") >= 0, "Should generate smooth oscillator")
assert(string.find(berry_code, "push_closure_step") >= 0, "Should generate closure step for restart")
assert(string.find(berry_code, "smooth_val_.start(engine.time_ms)") >= 0, "Should call start() method")
print("✓ Basic restart test passed")
return true
end
# Test reset/restart with different value provider types
def test_reset_restart_different_providers()
print("Testing reset/restart with different value provider types...")
var dsl_source = "set triangle_val = triangle(min_value=0, max_value=29, duration=5s)\n" +
"set cosine_val = cosine_osc(min_value=0, max_value=29, duration=5s)\n" +
"set sine_val = sine_osc(min_value=0, max_value=255, duration=2s)\n" +
"animation test_anim = solid(color=green)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 500ms\n" +
" reset triangle_val\n" +
" wait 200ms\n" +
" restart cosine_val\n" +
" wait 200ms\n" +
" reset sine_val\n" +
" play test_anim for 500ms\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for multiple providers")
assert(string.find(berry_code, "triangle_val_.start(engine.time_ms)") >= 0, "Should reset triangle")
assert(string.find(berry_code, "cosine_val_.start(engine.time_ms)") >= 0, "Should restart cosine")
assert(string.find(berry_code, "sine_val_.start(engine.time_ms)") >= 0, "Should reset sine")
# Count the number of closure steps - should be 3 (one for each reset/restart)
var closure_count = 0
var pos = 0
while true
pos = string.find(berry_code, "push_closure_step", pos)
if pos < 0 break end
closure_count += 1
pos += 1
end
assert(closure_count == 3, f"Should have 3 closure steps for reset/restart, found {closure_count}")
print("✓ Different providers test passed")
return true
end
# Test reset/restart with animations
def test_reset_restart_animations()
print("Testing reset/restart with animations...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=2s)\n" +
"animation pulse_anim = pulsating_animation(color=red, period=3s)\n" +
"animation solid_anim = solid(color=blue)\n" +
"\n" +
"sequence demo {\n" +
" play pulse_anim for 1s\n" +
" reset pulse_anim\n" +
" play solid_anim for 1s\n" +
" restart solid_anim\n" +
" play pulse_anim for 1s\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for animation reset/restart")
assert(string.find(berry_code, "pulse_anim_.start(engine.time_ms)") >= 0, "Should reset pulse animation")
assert(string.find(berry_code, "solid_anim_.start(engine.time_ms)") >= 0, "Should restart solid animation")
# Count the number of closure steps - should be 2 (one for each reset/restart)
var closure_count = 0
var pos = 0
while true
pos = string.find(berry_code, "push_closure_step", pos)
if pos < 0 break end
closure_count += 1
pos += 1
end
assert(closure_count == 2, f"Should have 2 closure steps for animation reset/restart, found {closure_count}")
print("✓ Animation reset/restart test passed")
return true
end
# Test reset/restart in repeat blocks
def test_reset_restart_in_repeat()
print("Testing reset/restart in repeat blocks...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=1s)\n" +
"animation test_anim = solid(color=yellow)\n" +
"\n" +
"sequence demo {\n" +
" repeat 3 times {\n" +
" play test_anim for 500ms\n" +
" reset osc_val\n" +
" wait 200ms\n" +
" }\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for reset in repeat")
assert(string.find(berry_code, "push_repeat_subsequence") >= 0, "Should generate repeat block")
assert(string.find(berry_code, "osc_val_.start(engine.time_ms)") >= 0, "Should reset in repeat block")
print("✓ Reset/restart in repeat test passed")
return true
end
# Test error handling - undefined value provider
def test_error_undefined_provider()
print("Testing error handling for undefined value provider...")
var dsl_source = "animation test_anim = solid(color=red)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset undefined_provider\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = nil
try
berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should fail with undefined provider error")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "Undefined reference 'undefined_provider'") >= 0, "Should report undefined reference error")
end
print("✓ Undefined provider error test passed")
return true
end
# Test error handling - non-value provider
def test_error_non_value_provider()
print("Testing error handling for non-value provider...")
var dsl_source = "color my_color = 0xFF0000\n" +
"animation test_anim = solid(color=red)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset my_color\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = nil
try
berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should fail with non-value provider error")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "is not a value provider") >= 0, "Should report non-value provider error")
end
print("✓ Non-value provider error test passed")
return true
end
# Test error handling - animation instead of value provider
def test_error_animation_not_provider()
print("Testing error handling for animation instead of value provider...")
var dsl_source = "animation my_anim = solid(color=blue)\n" +
"\n" +
"sequence demo {\n" +
" play my_anim for 1s\n" +
" restart my_anim\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = nil
try
berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should fail with animation not value provider error")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "is not a value provider") >= 0, "Should report animation not value provider error")
end
print("✓ Animation not provider error test passed")
return true
end
# Test error handling - variable instead of value provider
def test_error_variable_not_provider()
print("Testing error handling for variable instead of value provider...")
var dsl_source = "set my_var = 100\n" +
"animation test_anim = solid(color=green)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset my_var\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = nil
try
berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should fail with variable not value provider error")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "is not a value provider") >= 0, "Should report variable not value provider error")
end
print("✓ Variable not provider error test passed")
return true
end
# Test complex scenario with multiple resets/restarts
def test_complex_scenario()
print("Testing complex scenario with multiple resets/restarts...")
var dsl_source = "# Complex cylon eye with reset functionality\n" +
"set strip_len = strip_length()\n" +
"palette eye_palette = [ red, yellow, green, violet ]\n" +
"color eye_color = color_cycle(palette=eye_palette, cycle_period=0)\n" +
"set cosine_val = cosine_osc(min_value = 0, max_value = strip_len - 2, duration = 5s)\n" +
"set triangle_val = triangle(min_value = 0, max_value = strip_len - 2, duration = 5s)\n" +
"\n" +
"animation red_eye = beacon_animation(\n" +
" color = eye_color\n" +
" pos = cosine_val\n" +
" beacon_size = 3\n" +
" slew_size = 2\n" +
" priority = 10\n" +
")\n" +
"\n" +
"sequence cylon_eye {\n" +
" play red_eye for 3s\n" +
" red_eye.pos = triangle_val\n" +
" reset triangle_val\n" +
" play red_eye for 3s\n" +
" red_eye.pos = cosine_val\n" +
" restart cosine_val\n" +
" eye_color.next = 1\n" +
"}\n" +
"\n" +
"run cylon_eye"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile complex scenario")
assert(string.find(berry_code, "triangle_val_.start(engine.time_ms)") >= 0, "Should reset triangle_val")
assert(string.find(berry_code, "cosine_val_.start(engine.time_ms)") >= 0, "Should restart cosine_val")
# Should have multiple closure steps: 2 assignments + 2 resets + 1 color advance = 5 total
var closure_count = 0
var pos = 0
while true
pos = string.find(berry_code, "push_closure_step", pos)
if pos < 0 break end
closure_count += 1
pos += 1
end
assert(closure_count == 5, f"Should have 5 closure steps in complex scenario, found {closure_count}")
print("✓ Complex scenario test passed")
return true
end
# Test that reset/restart works with comments
def test_reset_restart_with_comments()
print("Testing reset/restart with comments...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=2s) # Triangle oscillator\n" +
"animation test_anim = solid(color=red)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset osc_val # Reset the oscillator\n" +
" restart osc_val # Restart it again\n" +
" play test_anim for 1s\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code with comments")
assert(string.find(berry_code, "# Reset the oscillator") >= 0, "Should preserve reset comment")
assert(string.find(berry_code, "# Restart it again") >= 0, "Should preserve restart comment")
# Should have 2 closure steps for reset and restart
var closure_count = 0
var pos = 0
while true
pos = string.find(berry_code, "push_closure_step", pos)
if pos < 0 break end
closure_count += 1
pos += 1
end
assert(closure_count == 2, f"Should have 2 closure steps, found {closure_count}")
print("✓ Reset/restart with comments test passed")
return true
end
# Run all tests
def run_all_reset_restart_tests()
print("Starting DSL Reset/Restart Tests...")
test_reset_basic()
test_restart_basic()
test_reset_restart_different_providers()
test_reset_restart_in_repeat()
test_error_undefined_provider()
test_error_non_value_provider()
test_error_animation_not_provider()
test_error_variable_not_provider()
test_complex_scenario()
test_reset_restart_with_comments()
print("\n🎉 All DSL Reset/Restart tests passed!")
return true
end
# Execute tests
run_all_reset_restart_tests()
return {
"run_all_reset_restart_tests": run_all_reset_restart_tests,
"test_reset_basic": test_reset_basic,
"test_restart_basic": test_restart_basic,
"test_reset_restart_different_providers": test_reset_restart_different_providers,
"test_reset_restart_in_repeat": test_reset_restart_in_repeat,
"test_error_undefined_provider": test_error_undefined_provider,
"test_error_non_value_provider": test_error_non_value_provider,
"test_error_animation_not_provider": test_error_animation_not_provider,
"test_error_variable_not_provider": test_error_variable_not_provider,
"test_complex_scenario": test_complex_scenario,
"test_reset_restart_with_comments": test_reset_restart_with_comments
}

View File

@ -29,7 +29,7 @@ def test_basic_transpilation()
assert(string.find(berry_code, "var engine = animation.init_strip()") >= 0, "Should generate strip configuration")
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should generate color definition")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should generate sequence manager")
assert(string.find(berry_code, "engine.add_sequence_manager(demo_)") >= 0, "Should add sequence manager")
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should add sequence manager")
# print("Generated Berry code:")
# print("==================================================")
@ -185,7 +185,7 @@ def test_sequence_assignments()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile sequence with assignments")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
assert(string.find(berry_code, ".push_assign_step") >= 0, "Should generate assign step")
assert(string.find(berry_code, ".push_closure_step") >= 0, "Should generate closure step")
assert(string.find(berry_code, "test_.opacity = brightness_") >= 0, "Should generate assignment")
# Test multiple assignments in sequence
@ -212,7 +212,7 @@ def test_sequence_assignments()
var assign_count = 0
var pos = 0
while true
pos = string.find(multi_berry_code, "push_assign_step", pos)
pos = string.find(multi_berry_code, "push_closure_step", pos)
if pos < 0 break end
assign_count += 1
pos += 1
@ -235,9 +235,10 @@ def test_sequence_assignments()
"run demo"
var repeat_berry_code = animation_dsl.compile(repeat_assign_dsl)
print(repeat_berry_code)
assert(repeat_berry_code != nil, "Should compile repeat with assignments")
assert(string.find(repeat_berry_code, "push_repeat_subsequence") >= 0, "Should generate repeat loop")
assert(string.find(repeat_berry_code, "push_assign_step") >= 0, "Should generate assign step in repeat")
assert(string.find(repeat_berry_code, "push_closure_step") >= 0, "Should generate closure step in repeat")
# Test complex cylon rainbow example
var cylon_dsl = "set strip_len = strip_length()\n" +
@ -375,9 +376,9 @@ def test_multiple_run_statements()
assert(start_count == 1, f"Should have exactly 1 engine.start() call, found {start_count}")
# Check that all animations are added to the engine
assert(string.find(berry_code, "engine.add_animation(red_anim_)") >= 0, "Should add red_anim to engine")
assert(string.find(berry_code, "engine.add_animation(blue_anim_)") >= 0, "Should add blue_anim to engine")
assert(string.find(berry_code, "engine.add_animation(green_anim_)") >= 0, "Should add green_anim to engine")
assert(string.find(berry_code, "engine.add(red_anim_)") >= 0, "Should add red_anim to engine")
assert(string.find(berry_code, "engine.add(blue_anim_)") >= 0, "Should add blue_anim to engine")
assert(string.find(berry_code, "engine.add(green_anim_)") >= 0, "Should add green_anim to engine")
# Verify the engine.start() comes after all animations are added
var start_line_index = -1
@ -388,7 +389,7 @@ def test_multiple_run_statements()
if string.find(line, "engine.start()") >= 0
start_line_index = i
end
if string.find(line, "engine.add_animation") >= 0 || string.find(line, "engine.add_sequence_manager") >= 0
if string.find(line, "engine.add(") >= 0
last_add_line_index = i
end
end
@ -425,8 +426,8 @@ def test_multiple_run_statements()
assert(mixed_start_count == 1, f"Mixed scenario should have exactly 1 engine.start() call, found {mixed_start_count}")
# Check that both animation and sequence are handled
assert(string.find(mixed_berry_code, "engine.add_animation(red_anim_)") >= 0, "Should add animation to engine")
assert(string.find(mixed_berry_code, "engine.add_sequence_manager(blue_seq_)") >= 0, "Should add sequence to engine")
assert(string.find(mixed_berry_code, "engine.add(red_anim_)") >= 0, "Should add animation to engine")
assert(string.find(mixed_berry_code, "engine.add(blue_seq_)") >= 0, "Should add sequence to engine")
print("✓ Multiple run statements test passed")
return true
@ -685,7 +686,7 @@ def test_complex_dsl()
assert(string.find(berry_code, "var engine = animation.init_strip()") >= 0, "Should have default strip initialization")
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should have color definitions")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should have sequence definition")
assert(string.find(berry_code, "engine.add_sequence_manager(demo_)") >= 0, "Should have execution")
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should have execution")
print("Generated code structure looks correct")
else
@ -1038,6 +1039,81 @@ def test_color_type_checking()
return true
end
# Test invalid sequence commands
def test_invalid_sequence_commands()
print("Testing invalid sequence commands...")
# Test 1: Invalid command in sequence
var invalid_command_dsl =
"animation test_anim = solid(color=red)\n" +
"sequence bad {\n" +
" do_bad_things anim\n" +
" play test_anim for 1s\n" +
"}"
try
var result1 = animation_dsl.compile(invalid_command_dsl)
assert(false, "Should have thrown an exception for invalid command")
except "dsl_compilation_error"
# Expected - invalid command should cause compilation error
end
# Test 2: Another invalid command
var invalid_command_dsl2 =
"animation test_anim = solid(color=red)\n" +
"sequence bad {\n" +
" play test_anim for 1s\n" +
" invalid_command\n" +
" wait 500ms\n" +
"}"
try
var result2 = animation_dsl.compile(invalid_command_dsl2)
assert(false, "Should have thrown an exception for invalid command")
except "dsl_compilation_error"
# Expected - invalid command should cause compilation error
end
# Test 3: Invalid command in repeat block
var invalid_repeat_dsl =
"animation test_anim = solid(color=red)\n" +
"sequence bad {\n" +
" repeat 3 times {\n" +
" play test_anim for 1s\n" +
" bad_command_in_repeat\n" +
" wait 500ms\n" +
" }\n" +
"}"
try
var result3 = animation_dsl.compile(invalid_repeat_dsl)
assert(false, "Should have thrown an exception for invalid command in repeat")
except "dsl_compilation_error"
# Expected - invalid command should cause compilation error
end
# Test 4: Valid sequence should still work
var valid_sequence_dsl =
"animation test_anim = solid(color=red)\n" +
"sequence good {\n" +
" play test_anim for 1s\n" +
" wait 500ms\n" +
" log(\"test message\")\n" +
" test_anim.opacity = 128\n" +
"}"
var result4 = animation_dsl.compile(valid_sequence_dsl)
assert(result4 != nil, "Should compile valid sequence successfully")
assert(string.find(result4, "SequenceManager") >= 0, "Should generate sequence manager")
assert(string.find(result4, "push_play_step") >= 0, "Should generate play step")
assert(string.find(result4, "push_wait_step") >= 0, "Should generate wait step")
assert(string.find(result4, "log(f\"test message\", 3)") >= 0, "Should generate log statement")
assert(string.find(result4, "push_closure_step") >= 0, "Should generate closure steps")
print("✓ Invalid sequence commands test passed")
return true
end
# Run all tests
def run_dsl_transpiler_tests()
print("=== DSL Transpiler Test Suite ===")
@ -1064,7 +1140,8 @@ def run_dsl_transpiler_tests()
test_comment_preservation,
test_easing_keywords,
test_animation_type_checking,
test_color_type_checking
test_color_type_checking,
test_invalid_sequence_commands
]
var passed = 0

View File

@ -173,7 +173,7 @@ fb1.fill_pixels(0xFF0000FF) # Fill fb1 with red (fully opaque)
fb2.fill_pixels(0x8000FF00) # Fill fb2 with green at 50% alpha
# Blend fb2 into fb1 using per-pixel alpha, but only for the first half
fb1.blend_pixels(fb2, animation.frame_buffer.BLEND_MODE_NORMAL, 0, 4)
fb1.blend_pixels(fb2, 0, 4)
var first_half_blended = true
var second_half_original = true

View File

@ -59,28 +59,6 @@ def test_palette_with_named_colors()
print("✓ Palette with named colors test passed")
end
# Test palette with custom colors
def test_palette_with_custom_colors()
print("Testing palette with custom colors...")
var dsl_source =
"# Define custom colors first\n" +
"color aurora_green = 0x00AA44\n" +
"color aurora_purple = 0x8800AA\n" +
"\n" +
"palette aurora_palette = [\n" +
" (0, 0x000022), # Dark night sky\n" +
" (64, aurora_green), # Custom green\n" +
" (192, aurora_purple), # Custom purple\n" +
" (255, 0xCCAAFF) # Pale purple\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "DSL compilation with custom colors should succeed")
print("✓ Palette with custom colors test passed")
end
# Test error handling for invalid palette syntax
def test_palette_error_handling()
print("Testing palette error handling...")
@ -627,6 +605,76 @@ def test_alternative_syntax_integration()
print("✓ Alternative syntax integration test passed")
end
# Test that non-predefined colors raise exceptions
# Palettes only accept hex colors (0xRRGGBB) or predefined color names,
# but not custom colors defined previously. For dynamic palettes, use user functions.
def test_non_predefined_color_exceptions()
print("Testing non-predefined color exceptions...")
# Test 1: Custom color identifier in tuple syntax should fail
try
var custom_color_tuple = "palette test1 = [(0, custom_red)]"
var result1 = animation_dsl.compile(custom_color_tuple)
assert(result1 == nil, "Should fail with custom color identifier in tuple syntax")
print("✗ FAIL: Custom color identifier in tuple syntax was accepted")
return false
except .. as e, msg
# Expected to fail - custom color identifier not allowed
print("✓ Custom color identifier in tuple syntax correctly rejected")
end
# Test 2: Custom color identifier in alternative syntax should fail
try
var custom_color_alt = "palette test2 = [red, custom_blue, green]"
var result2 = animation_dsl.compile(custom_color_alt)
assert(result2 == nil, "Should fail with custom color identifier in alternative syntax")
print("✗ FAIL: Custom color identifier in alternative syntax was accepted")
return false
except .. as e, msg
# Expected to fail - custom color identifier not allowed
print("✓ Custom color identifier in alternative syntax correctly rejected")
end
# Test 3: The specific case from the user report - 'grrreen' should fail
try
var grrreen_case = "palette rainbow_with_white = [red, grrreen]"
var result3 = animation_dsl.compile(grrreen_case)
assert(result3 == nil, "Should fail with 'grrreen' identifier")
print("✗ FAIL: 'grrreen' identifier was accepted")
return false
except .. as e, msg
# Expected to fail - 'grrreen' is not a predefined color
print("✓ 'grrreen' identifier correctly rejected")
end
# Test 4: Misspelled predefined color should fail
try
var misspelled = "palette test4 = [red, bleu, green]" # 'bleu' instead of 'blue'
var result4 = animation_dsl.compile(misspelled)
assert(result4 == nil, "Should fail with misspelled color 'bleu'")
print("✗ FAIL: Misspelled color 'bleu' was accepted")
return false
except .. as e, msg
# Expected to fail - 'bleu' is not a predefined color
print("✓ Misspelled color 'bleu' correctly rejected")
end
# Test 5: Random identifier should fail
try
var random_id = "palette test5 = [red, some_random_name, blue]"
var result5 = animation_dsl.compile(random_id)
assert(result5 == nil, "Should fail with random identifier")
print("✗ FAIL: Random identifier was accepted")
return false
except .. as e, msg
# Expected to fail - random identifier is not a predefined color
print("✓ Random identifier correctly rejected")
end
print("✓ Non-predefined color exceptions test passed")
return true
end
# Run all palette tests
def run_palette_tests()
print("=== Palette DSL Tests ===")
@ -635,9 +683,9 @@ def run_palette_tests()
test_palette_keyword_recognition()
test_palette_definition()
test_palette_with_named_colors()
test_palette_with_custom_colors()
test_palette_error_handling()
test_nonexistent_color_names()
test_non_predefined_color_exceptions() # New test for strict color validation
test_palette_integration()
test_vrgb_format_validation()
test_complete_workflow()

View File

@ -216,12 +216,6 @@ def test_parameter_metadata()
assert(enum_meta.contains("enum"), "Should have enum constraint")
assert(enum_meta["default"] == 1, "Should have default value")
# Test getting all metadata
var all_meta = obj.get_params_metadata()
assert(all_meta.contains("range_param"), "Should contain range_param metadata")
assert(all_meta.contains("enum_param"), "Should contain enum_param metadata")
assert(all_meta.contains("simple_param"), "Should contain simple_param metadata")
print("✓ Parameter metadata test passed")
end

View File

@ -62,12 +62,12 @@ def test_sequence_manager_step_creation()
assert(wait_step["type"] == "wait", "Wait step should have correct type")
assert(wait_step["duration"] == 2000, "Wait step should have correct duration")
# Test push_assign_step
# Test push_closure_step
var test_closure = def (engine) test_anim.opacity = 128 end
seq_manager.push_assign_step(test_closure)
assert(seq_manager.steps.size() == 3, "Should have three steps after push_assign_step")
seq_manager.push_closure_step(test_closure)
assert(seq_manager.steps.size() == 3, "Should have three steps after push_closure_step")
var assign_step = seq_manager.steps[2]
assert(assign_step["type"] == "assign", "Assign step should have correct type")
assert(assign_step["type"] == "closure", "Assign step should have correct type")
assert(assign_step["closure"] == test_closure, "Assign step should have correct closure")
print("✓ Step creation tests passed")
@ -318,7 +318,7 @@ def test_sequence_manager_assignment_steps()
# Create sequence with assignment step using fluent interface
seq_manager.push_play_step(test_anim, 500) # Play for 0.5s
.push_assign_step(assignment_closure) # Assign new opacity
.push_closure_step(assignment_closure) # Assign new opacity
.push_play_step(test_anim, 500) # Play for another 0.5s
# Start sequence
@ -475,6 +475,247 @@ def test_sequence_manager_integration()
print("✓ Integration tests passed")
end
def test_sequence_manager_parametric_repeat_counts()
print("=== SequenceManager Parametric Repeat Count Tests ===")
# Create strip and engine
var strip = global.Leds(30)
var engine = animation.create_engine(strip)
# Test 1: Static repeat count (baseline)
var static_repeat_count = 3
var seq_manager1 = animation.SequenceManager(engine, static_repeat_count)
# Test get_resolved_repeat_count with static number
var resolved_count = seq_manager1.get_resolved_repeat_count()
assert(resolved_count == 3, f"Static repeat count should resolve to 3, got {resolved_count}")
# Test 2: Function-based repeat count (simulating col1.palette_size)
var palette_size_function = def (engine) return 5 end # Simulates a palette with 5 colors
var seq_manager2 = animation.SequenceManager(engine, palette_size_function)
# Test get_resolved_repeat_count with function
resolved_count = seq_manager2.get_resolved_repeat_count()
assert(resolved_count == 5, f"Function repeat count should resolve to 5, got {resolved_count}")
# Test 3: Dynamic repeat count that changes over time
var dynamic_counter = 0
var dynamic_function = def (engine)
dynamic_counter += 1
return dynamic_counter <= 1 ? 2 : 4 # First call returns 2, subsequent calls return 4
end
var seq_manager3 = animation.SequenceManager(engine, dynamic_function)
var first_resolved = seq_manager3.get_resolved_repeat_count()
var second_resolved = seq_manager3.get_resolved_repeat_count()
assert(first_resolved == 2, f"First dynamic call should return 2, got {first_resolved}")
assert(second_resolved == 4, f"Second dynamic call should return 4, got {second_resolved}")
print("✓ Parametric repeat count tests passed")
end
def test_sequence_manager_repeat_execution_with_functions()
print("=== SequenceManager Repeat Execution with Functions Tests ===")
# Create strip and engine
var strip = global.Leds(30)
var engine = animation.create_engine(strip)
# Create test animation
var color_provider = animation.static_color(engine)
color_provider.color = 0xFF00FF00
var test_anim = animation.solid(engine)
test_anim.color = color_provider
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test_repeat"
# Create a function that returns repeat count (simulating palette_size)
var repeat_count_func = def (engine) return 3 end
# Create sequence manager with function-based repeat count
var seq_manager = animation.SequenceManager(engine, repeat_count_func)
seq_manager.push_play_step(test_anim, 50) # Short duration for testing
# Verify repeat count is resolved correctly
var resolved_count = seq_manager.get_resolved_repeat_count()
assert(resolved_count == 3, f"Repeat count should resolve to 3, got {resolved_count}")
# Test that the sequence manager accepts function-based repeat counts
assert(type(seq_manager.repeat_count) == "function", "Repeat count should be stored as function")
# Test that multiple calls to get_resolved_repeat_count work
var second_resolved = seq_manager.get_resolved_repeat_count()
assert(second_resolved == 3, f"Second resolution should also return 3, got {second_resolved}")
# Test sequence execution with function-based repeat count
tasmota.set_millis(90000)
seq_manager.start(90000)
assert(seq_manager.is_running == true, "Sequence should start running")
assert(seq_manager.current_iteration == 0, "Should start at iteration 0")
print("✓ Repeat execution with functions tests passed")
end
def test_sequence_manager_palette_size_simulation()
print("=== SequenceManager Palette Size Simulation Tests ===")
# Create strip and engine
var strip = global.Leds(30)
var engine = animation.create_engine(strip)
# Simulate a color cycle with palette_size property (like col1.palette_size)
var mock_color_cycle = {
"palette_size": 5, # Use smaller palette for simpler testing
"current_index": 0
}
# Create function that accesses palette_size (simulating col1.palette_size)
var palette_size_func = def (engine) return mock_color_cycle["palette_size"] end
# Create closure that advances color cycle (simulating col1.next = 1)
var advance_color_func = def (engine)
mock_color_cycle["current_index"] = (mock_color_cycle["current_index"] + 1) % mock_color_cycle["palette_size"]
end
# Create sequence similar to demo_shutter_rainbow.anim:
# sequence shutter_seq repeat col1.palette_size times {
# play shutter_animation for duration
# col1.next = 1
# }
var seq_manager = animation.SequenceManager(engine, palette_size_func)
seq_manager.push_closure_step(advance_color_func) # Just test the closure execution
# Test that repeat count is resolved correctly
var resolved_count = seq_manager.get_resolved_repeat_count()
assert(resolved_count == 5, f"Should resolve to palette size 5, got {resolved_count}")
# Test that the closure function works
var initial_index = mock_color_cycle["current_index"]
advance_color_func(engine)
assert(mock_color_cycle["current_index"] == (initial_index + 1) % 5, "Color should advance when closure is called")
# Test that the function can be called multiple times
var second_resolved = seq_manager.get_resolved_repeat_count()
assert(second_resolved == 5, f"Second resolution should also return 5, got {second_resolved}")
print("✓ Palette size simulation tests passed")
end
def test_sequence_manager_dynamic_repeat_changes()
print("=== SequenceManager Dynamic Repeat Changes Tests ===")
# Create strip and engine
var strip = global.Leds(30)
var engine = animation.create_engine(strip)
# Create test animation
var color_provider = animation.static_color(engine)
color_provider.color = 0xFF0080FF
var test_anim = animation.solid(engine)
test_anim.color = color_provider
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "dynamic_test"
# Create dynamic repeat count that changes based on external state
var external_state = {"multiplier": 2}
var dynamic_repeat_func = def (engine)
return external_state["multiplier"] * 2 # Returns 4 initially, can change
end
# Create sequence with dynamic repeat count
var seq_manager = animation.SequenceManager(engine, dynamic_repeat_func)
seq_manager.push_play_step(test_anim, 250)
# Start sequence
tasmota.set_millis(120000)
engine.add_sequence_manager(seq_manager)
engine.start()
engine.on_tick(120000)
seq_manager.start(120000)
# Test initial repeat count resolution
var initial_count = seq_manager.get_resolved_repeat_count()
assert(initial_count == 4, f"Initial repeat count should be 4, got {initial_count}")
# Change external state mid-execution (simulating dynamic conditions)
external_state["multiplier"] = 3
# Test that new calls get updated count
var updated_count = seq_manager.get_resolved_repeat_count()
assert(updated_count == 6, f"Updated repeat count should be 6, got {updated_count}")
# Test with function that depends on engine state
var engine_dependent_func = def (engine)
# Simulating a function that depends on strip length or other engine properties
return engine.strip != nil ? 3 : 1
end
var seq_manager2 = animation.SequenceManager(engine, engine_dependent_func)
var engine_count = seq_manager2.get_resolved_repeat_count()
assert(engine_count == 3, f"Engine-dependent count should be 3, got {engine_count}")
print("✓ Dynamic repeat changes tests passed")
end
def test_sequence_manager_complex_parametric_scenario()
print("=== SequenceManager Complex Parametric Scenario Tests ===")
# Create strip and engine
var strip = global.Leds(30)
var engine = animation.create_engine(strip)
# Simulate complex scenario with multiple parametric elements
# Similar to a more complex version of demo_shutter_rainbow.anim
# Mock palette and color cycle objects
var rainbow_palette = {
"colors": [0xFFFF0000, 0xFFFF8000, 0xFFFFFF00], # Smaller palette for testing
"size": 3
}
var color_cycle1 = {
"palette": rainbow_palette,
"current_index": 0,
"palette_size": rainbow_palette["size"]
}
# Functions for parametric behavior
var palette_size_func = def (engine) return color_cycle1["palette_size"] end
var advance_colors_func = def (engine)
color_cycle1["current_index"] = (color_cycle1["current_index"] + 1) % color_cycle1["palette_size"]
end
# Create sequence with parametric repeat
var seq_manager = animation.SequenceManager(engine, palette_size_func)
seq_manager.push_closure_step(advance_colors_func)
# Verify sequence setup
var resolved_count = seq_manager.get_resolved_repeat_count()
assert(resolved_count == 3, f"Complex sequence should repeat 3 times, got {resolved_count}")
# Test that the functions work correctly
var initial_color_index = color_cycle1["current_index"]
# Test closure execution
advance_colors_func(engine)
assert(color_cycle1["current_index"] == (initial_color_index + 1) % 3, "Color should advance")
# Test multiple function calls
var second_resolved = seq_manager.get_resolved_repeat_count()
assert(second_resolved == 3, f"Second resolution should still return 3, got {second_resolved}")
# Test that palette size function works with different values
color_cycle1["palette_size"] = 5
var updated_resolved = seq_manager.get_resolved_repeat_count()
assert(updated_resolved == 5, f"Updated resolution should return 5, got {updated_resolved}")
print("✓ Complex parametric scenario tests passed")
end
# Run all tests
def run_all_sequence_manager_tests()
print("Starting SequenceManager Unit Tests...")
@ -489,6 +730,11 @@ def run_all_sequence_manager_tests()
test_sequence_manager_assignment_steps()
test_sequence_manager_complex_sequence()
test_sequence_manager_integration()
test_sequence_manager_parametric_repeat_counts()
test_sequence_manager_repeat_execution_with_functions()
test_sequence_manager_palette_size_simulation()
test_sequence_manager_dynamic_repeat_changes()
test_sequence_manager_complex_parametric_scenario()
print("\n🎉 All SequenceManager tests passed!")
return true
@ -508,5 +754,10 @@ return {
"test_sequence_manager_is_running": test_sequence_manager_is_running,
"test_sequence_manager_assignment_steps": test_sequence_manager_assignment_steps,
"test_sequence_manager_complex_sequence": test_sequence_manager_complex_sequence,
"test_sequence_manager_integration": test_sequence_manager_integration
"test_sequence_manager_integration": test_sequence_manager_integration,
"test_sequence_manager_parametric_repeat_counts": test_sequence_manager_parametric_repeat_counts,
"test_sequence_manager_repeat_execution_with_functions": test_sequence_manager_repeat_execution_with_functions,
"test_sequence_manager_palette_size_simulation": test_sequence_manager_palette_size_simulation,
"test_sequence_manager_dynamic_repeat_changes": test_sequence_manager_dynamic_repeat_changes,
"test_sequence_manager_complex_parametric_scenario": test_sequence_manager_complex_parametric_scenario
}

View File

@ -127,15 +127,9 @@ def test_parameterized_object_integration()
assert(provider.engine == engine, "Provider should have correct engine reference")
# Test parameter system methods exist
assert(type(provider.get_params_metadata) == "function", "Should have get_params_metadata method")
assert(type(provider.set_param) == "function", "Should have set_param method")
assert(type(provider.get_param) == "function", "Should have get_param method")
# Test parameter metadata
var params = provider.get_params_metadata()
assert(params.contains("value"), "Should have 'value' parameter defined")
assert(params["value"]["default"] == nil, "Default value should be nil")
# Test parameter setting via parameter system
assert(provider.set_param("value", 777) == true, "Should be able to set value via parameter system")
assert(provider.get_param("value", nil) == 777, "Should retrieve value via parameter system")

View File

@ -60,6 +60,7 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/breathe_animation_test.be",
"lib/libesp32/berry_animation/src/tests/color_cycle_animation_test.be",
"lib/libesp32/berry_animation/src/tests/color_cycle_bytes_test.be", # Tests ColorCycleColorProvider with bytes palette
"lib/libesp32/berry_animation/src/tests/color_cycle_palette_size_test.be", # Tests ColorCycleColorProvider palette_size read-only parameter
"lib/libesp32/berry_animation/src/tests/rich_palette_animation_test.be",
"lib/libesp32/berry_animation/src/tests/rich_palette_animation_class_test.be",
"lib/libesp32/berry_animation/src/tests/comet_animation_test.be",

View File

@ -98,7 +98,6 @@ def test_parameterized_object_integration()
assert(provider.engine == engine, "Provider should have correct engine reference")
# Test parameter system methods exist
assert(type(provider.get_params_metadata) == "function", "Should have get_params_metadata method")
assert(type(provider.set_param) == "function", "Should have set_param method")
assert(type(provider.get_param) == "function", "Should have get_param method")