Berry animation fix timings (#23869)

This commit is contained in:
s-hadinger 2025-09-03 22:10:31 +02:00 committed by GitHub
parent 9917555c6f
commit a52fdd0526
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
107 changed files with 14669 additions and 13985 deletions

View File

@ -37,7 +37,7 @@ 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(demo_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -47,7 +47,7 @@ breathing_.opacity = (def (engine)
end)(engine)
# Start the animation
engine.add(breathing_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -150,7 +150,7 @@ engine.add(stripe7_)
engine.add(stripe8_)
engine.add(stripe9_)
engine.add(stripe10_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -72,7 +72,7 @@ engine.add(ornaments_)
engine.add(tree_star_)
engine.add(snow_sparkles_)
engine.add(garland_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -40,7 +40,7 @@ engine.add(background_)
engine.add(comet_main_)
engine.add(comet_secondary_)
engine.add(comet_sparkles_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -16,24 +16,24 @@ var strip_len_ = animation.strip_length(engine)
# Create animation with computed values
var stream1_ = animation.comet_animation(engine)
stream1_.color = 0xFFFF0000
stream1_.tail_length = animation.create_closure_value(engine, def (self) return self.abs(self.resolve(strip_len_) / 4) end) # computed value
stream1_.tail_length = animation.create_closure_value(engine, def (engine) return animation._math.abs(animation.resolve(strip_len_) / 4) end) # computed value
stream1_.speed = 1.5
stream1_.priority = 10
# More complex computed values
var base_speed_ = 2.0
var stream2_ = animation.comet_animation(engine)
stream2_.color = 0xFF0000FF
stream2_.tail_length = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) / 8 + (2 * self.resolve(strip_len_)) - 10 end) # computed with addition
stream2_.speed = animation.create_closure_value(engine, def (self) return self.resolve(base_speed_) * 1.5 end) # computed with multiplication
stream2_.tail_length = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) / 8 + (2 * animation.resolve(strip_len_)) - 10 end) # computed with addition
stream2_.speed = animation.create_closure_value(engine, def (engine) return animation.resolve(base_speed_) * 1.5 end) # computed with multiplication
stream2_.direction = (-1)
stream2_.priority = 5
# Property assignment with computed values
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)
stream1_.tail_length = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) / 5 end)
stream2_.opacity = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) * 4 end)
# Run both animations
engine.add(stream1_)
engine.add(stream2_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -20,7 +20,7 @@ def cylon_effect_template(engine, eye_color_, back_color_, duration_)
eye_animation_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = duration_
return provider
end)(engine)
@ -33,7 +33,7 @@ end
animation.register_user_function('cylon_effect', cylon_effect_template)
cylon_effect_template(engine, 0xFFFF0000, 0x00000000, 3000)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -20,14 +20,14 @@ eye_color_.cycle_period = 0
var cosine_val_ = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = eye_duration_
return provider
end)(engine)
var triangle_val_ = (def (engine)
var provider = animation.triangle(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = eye_duration_
return provider
end)(engine)
@ -43,7 +43,7 @@ var cylon_eye_ = animation.SequenceManager(engine, -1)
.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()
engine.run()
#- Original DSL source:

View File

@ -18,14 +18,14 @@ red_eye_.color = 0xFFFF0000
red_eye_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = 5000
return provider
end)(engine)
red_eye_.beacon_size = 3 # small 3 pixels eye
red_eye_.slew_size = 2 # with 2 pixel shading around
engine.add(red_eye_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -26,7 +26,7 @@ background_.priority = 20
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.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = 6000
return provider
end)(engine)
@ -39,11 +39,11 @@ eye_mask_.slew_size = 2 # with 2 pixel shading around
eye_mask_.priority = 5
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_.spatial_period = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) / 4 end)
fire_pattern_.opacity = eye_mask_
engine.add(background_)
engine.add(fire_pattern_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -73,7 +73,7 @@ var rainbow_with_white_ = bytes(
"FFFFFFFF"
)
shutter_bidir_template(engine, rainbow_with_white_, 1500)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -44,7 +44,7 @@ var shutter_run_ = animation.SequenceManager(engine, -1)
.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()
engine.run()
#- Original DSL source:

View File

@ -0,0 +1,144 @@
# Generated Berry code from Animation DSL
# Source: demo_shutter_rainbow_bidir.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_bidir
def shutter_bidir_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
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.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_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_bidir', shutter_bidir_template)
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
"FFFFFF00"
"FF008000" # comma left on-purpose to test transpiler
"FF0000FF" # need for a lighter blue
"FF4B0082"
"FFFFFFFF"
)
shutter_bidir_template(engine, rainbow_with_white_, 1500)
engine.run()
#- Original DSL source:
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
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
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving from right to left
animation shutter_rl_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_bidir(rainbow_with_white, 1.5s)
-#

View File

@ -0,0 +1,112 @@
# Generated Berry code from Animation DSL
# Source: demo_shutter_rainbow_central.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 center to both left and right
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_central
def shutter_central_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var strip_len2_ = animation.create_closure_value(engine, def (engine) return (animation.strip_length(engine) + 1) / 2 end)
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
# shutter moving from left to right
var shutter_central_animation_ = animation.beacon_animation(engine)
shutter_central_animation_.color = col2_
shutter_central_animation_.back_color = col1_
shutter_central_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - animation.resolve(shutter_size_) / 2 end)
shutter_central_animation_.beacon_size = shutter_size_
shutter_central_animation_.slew_size = 0
shutter_central_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_central_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_central', shutter_central_template)
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
"FFFFFF00"
"FF008000" # comma left on-purpose to test transpiler
"FF0000FF" # need for a lighter blue
"FF4B0082"
"FFFFFFFF"
)
shutter_central_template(engine, rainbow_with_white_, 1500)
engine.run()
#- Original DSL source:
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
template shutter_central {
param colors type palette
param duration
set strip_len = strip_length()
set strip_len2 = (strip_length() + 1) / 2
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
# shutter moving from left to right
animation shutter_central_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - shutter_size / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_central(rainbow_with_white, 1.5s)
-#

View File

@ -0,0 +1,110 @@
# Generated Berry code from Animation DSL
# Source: demo_shutter_rainbow_leftright.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, then right to left
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_lr
def shutter_lr_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
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.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)
engine.add(shutter_seq_)
end
animation.register_user_function('shutter_lr', shutter_lr_template)
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
"FFFFFF00"
"FF008000" # comma left on-purpose to test transpiler
"FF0000FF" # need for a lighter blue
"FF4B0082"
"FFFFFFFF"
)
shutter_lr_template(engine, rainbow_with_white_, 1500)
engine.run()
#- Original DSL source:
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_lr {
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
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_lr(rainbow_with_white, 1.5s)
-#

View File

@ -84,7 +84,7 @@ engine.add(disco_base_)
engine.add(white_flash_)
engine.add(disco_sparkles_)
engine.add(disco_pulse_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -48,7 +48,7 @@ fire_flicker_.priority = 10
# Start both animations
engine.add(fire_base_)
engine.add(fire_flicker_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -77,7 +77,7 @@ engine.add(heart_glow_)
engine.add(heartbeat1_)
engine.add(heartbeat2_)
engine.add(center_pulse_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -16,13 +16,13 @@ import user_functions
# Create animations that use imported user functions
var random_red_ = animation.solid(engine)
random_red_.color = 0xFFFF0000
random_red_.opacity = animation.create_closure_value(engine, def (self) return animation.get_user_function('rand_demo')(self.engine) end)
random_red_.opacity = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_demo')(engine) end)
var breathing_blue_ = animation.solid(engine)
breathing_blue_.color = 0xFF0000FF
breathing_blue_.opacity = animation.create_closure_value(engine, def (self) return self.max(50, self.min(255, animation.get_user_function('rand_demo')(self.engine) + 100)) end)
breathing_blue_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.max(50, animation._math.min(255, animation.get_user_function('rand_demo')(engine) + 100)) end)
var dynamic_green_ = animation.solid(engine)
dynamic_green_.color = 0xFF008000
dynamic_green_.opacity = animation.create_closure_value(engine, def (self) return self.abs(animation.get_user_function('rand_demo')(self.engine) - 128) + 64 end)
dynamic_green_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.abs(animation.get_user_function('rand_demo')(engine) - 128) + 64 end)
# Create a sequence that cycles through the animations
var import_demo_ = animation.SequenceManager(engine)
.push_play_step(random_red_, 3000)
@ -30,7 +30,7 @@ var import_demo_ = animation.SequenceManager(engine)
.push_play_step(dynamic_green_, 3000)
# Run the demo
engine.add(import_demo_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -98,7 +98,7 @@ engine.add(lava_blob1_)
engine.add(lava_blob2_)
engine.add(lava_blob3_)
engine.add(heat_shimmer_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -76,7 +76,7 @@ engine.add(lightning_main_)
engine.add(lightning_partial_)
engine.add(afterglow_)
engine.add(distant_flash_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -68,7 +68,7 @@ engine.add(stream1_)
engine.add(stream2_)
engine.add(stream3_)
engine.add(code_flash_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -57,7 +57,7 @@ engine.add(meteor2_)
engine.add(meteor3_)
engine.add(meteor4_)
engine.add(meteor_flash_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -83,7 +83,7 @@ engine.add(segment1_)
engine.add(segment2_)
engine.add(segment3_)
engine.add(arc_sparkles_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -74,7 +74,7 @@ engine.add(ocean_base_)
engine.add(wave1_)
engine.add(wave2_)
engine.add(foam_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -44,7 +44,7 @@ var palette_demo_ = animation.SequenceManager(engine)
.push_play_step(forest_anim_, 3000)
)
engine.add(palette_demo_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -89,7 +89,7 @@ var palette_showcase_ = animation.SequenceManager(engine)
.push_play_step(sunset_glow_, 2000)
)
engine.add(palette_showcase_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -95,7 +95,7 @@ engine.add(plasma_base_)
engine.add(plasma_wave1_)
engine.add(plasma_wave2_)
engine.add(plasma_wave3_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -60,7 +60,7 @@ white_strobe_.priority = 20
engine.add(left_red_)
engine.add(right_blue_)
engine.add(white_strobe_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -56,7 +56,7 @@ var demo_ = animation.SequenceManager(engine)
.push_wait_step(1000)
)
engine.add(demo_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -21,7 +21,7 @@ var rainbow_animation_ = animation.solid(engine)
rainbow_animation_.color = rainbow_cycle_
# Start the animation
engine.add(rainbow_animation_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -51,7 +51,7 @@ scanner_trail_.opacity = 128 # Half brightness
engine.add(background_)
engine.add(scanner_trail_)
engine.add(scanner_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -16,14 +16,14 @@ var strip_len_ = animation.strip_length(engine)
var triangle_val_ = (def (engine)
var provider = animation.triangle(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = 5000
return provider
end)(engine)
var cosine_val_ = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = 5000
return provider
end)(engine)
@ -110,7 +110,7 @@ var main_demo_ = animation.SequenceManager(engine)
.push_play_step(pulse_demo_, 1000)
# Run the main demo
engine.add(main_demo_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -22,7 +22,7 @@ rainbow_cycle_.cycle_period = 3000
var demo_ = animation.SequenceManager(engine)
.push_play_step(rainbow_cycle_, 15000)
engine.add(demo_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -88,7 +88,7 @@ engine.add(daylight_cycle_)
engine.add(sun_position_)
engine.add(sun_glow_)
engine.add(stars_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -21,7 +21,7 @@ var slide_colors_ = animation.SequenceManager(engine)
.push_play_step(swipe_animation_, 1000)
.push_closure_step(def (engine) olivary_.next = 1 end)
engine.add(slide_colors_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -37,7 +37,7 @@ var fire_palette_ = bytes("00000000" "80FF0000" "FFFFFF00")
var ocean_palette_ = bytes("00000080" "800080FF" "FF00FFFF")
# Use the template
rainbow_pulse_template(engine, fire_palette_, ocean_palette_, 3000, 0xFF001100)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -0,0 +1,144 @@
# Generated Berry code from Animation DSL
# Source: test_shutter_rainbow_bidir.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_bidir
def shutter_bidir_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 = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) + 0 end)
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
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(shutter_size_) + 0 end)
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = animation.create_closure_value(engine, def (engine) return 0 + 0 end)
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.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_bidir', shutter_bidir_template)
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
"FFFFFF00"
"FF008000" # comma left on-purpose to test transpiler
"FF0000FF" # need for a lighter blue
"FF4B0082"
"FFFFFFFF"
)
shutter_bidir_template(engine, rainbow_with_white_, 1500)
engine.run()
#- Original DSL source:
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len + 0, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size + 0
slew_size = 0
priority = 5
)
# shutter moving from right to left
animation shutter_rl_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = strip_len - shutter_size
slew_size = 0 + 0
priority = 5
)
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_bidir(rainbow_with_white, 1.5s)
-#

View File

@ -24,7 +24,7 @@ animation.register_user_function('pulse_effect', pulse_effect_template)
# Use the template - templates add animations directly to engine and run them
pulse_effect_template(engine, 0xFFFF0000, 2000, 204)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -24,7 +24,7 @@ animation.register_user_function('pulse_effect', pulse_effect_template)
# Use the template - templates add animations directly to engine and run them
pulse_effect_template(engine, 0xFFFF0000, 2000, 204)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -32,7 +32,7 @@ bright_flash_.priority = 15
engine.add(background_)
engine.add(stars_)
engine.add(bright_flash_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -19,38 +19,38 @@ var random_base_ = animation.solid(engine)
random_base_.color = 0xFF0000FF
random_base_.priority = 10
# Use user function in property assignment
random_base_.opacity = animation.create_closure_value(engine, def (self) return animation.get_user_function('rand_demo')(self.engine) end)
random_base_.opacity = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_demo')(engine) end)
# Example 2: User function with mathematical operations
var random_bounded_ = animation.solid(engine)
random_bounded_.color = 0xFFFFA500
random_bounded_.priority = 8
# User function with bounds using math functions
random_bounded_.opacity = animation.create_closure_value(engine, def (self) return self.max(50, self.min(255, animation.get_user_function('rand_demo')(self.engine) + 100)) end)
random_bounded_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.max(50, animation._math.min(255, animation.get_user_function('rand_demo')(engine) + 100)) end)
# Example 3: User function in arithmetic expressions
var random_variation_ = animation.solid(engine)
random_variation_.color = 0xFF800080
random_variation_.priority = 15
# Mix user function with arithmetic operations
random_variation_.opacity = animation.create_closure_value(engine, def (self) return self.abs(animation.get_user_function('rand_demo')(self.engine) - 128) + 64 end)
random_variation_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.abs(animation.get_user_function('rand_demo')(engine) - 128) + 64 end)
# Example 4: User function affecting different properties
var random_multi_ = animation.solid(engine)
random_multi_.color = 0xFF00FFFF
random_multi_.priority = 12
# Use user function for multiple properties
random_multi_.opacity = animation.create_closure_value(engine, def (self) return self.max(100, animation.get_user_function('rand_demo')(self.engine)) end)
random_multi_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.max(100, animation.get_user_function('rand_demo')(engine)) end)
# Example 5: Complex expression with user function
var random_complex_ = animation.solid(engine)
random_complex_.color = 0xFFFFFFFF
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)
random_complex_.opacity = animation.create_closure_value(engine, def (engine) return animation._math.round((animation.get_user_function('rand_demo')(engine) + 128) / 2 + animation._math.abs(animation.get_user_function('rand_demo')(engine) - 100)) end)
# Run all animations to demonstrate the effects
engine.add(random_base_)
engine.add(random_bounded_)
engine.add(random_variation_)
engine.add(random_multi_)
engine.add(random_complex_)
engine.start()
engine.run()
#- Original DSL source:

View File

@ -1,4 +1,4 @@
# Demo Shutter Rainbow
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
@ -35,13 +35,13 @@ template shutter_bidir {
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
reset shutter_size
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
reset shutter_size
restart shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1

View File

@ -0,0 +1,47 @@
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
template shutter_central {
param colors type palette
param duration
set strip_len = strip_length()
set strip_len2 = (strip_length() + 1) / 2
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
# shutter moving from left to right
animation shutter_central_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - shutter_size / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_central(rainbow_with_white, 1.5s)

View File

@ -0,0 +1,46 @@
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_lr {
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
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_lr(rainbow_with_white, 1.5s)

View File

@ -0,0 +1,63 @@
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len + 0, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size + 0
slew_size = 0
priority = 5
)
# shutter moving from right to left
animation shutter_rl_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = strip_len - shutter_size
slew_size = 0 + 0
priority = 5
)
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_bidir(rainbow_with_white, 1.5s)

View File

@ -79,6 +79,8 @@ Unified base class for all visual elements. Inherits from `ParameterizedObject`.
**Special Behavior**: Setting `is_running = true/false` starts/stops the animation.
**Timing Behavior**: The `start()` method only resets the time origin if the animation was already started previously (i.e., `self.start_time` is not nil). The first actual rendering tick occurs in `update()` or `render()` methods, which initialize `start_time` on first call.
**Factory**: `animation.animation(engine)`
## Value Providers
@ -93,6 +95,8 @@ Base interface for all value providers. Inherits from `ParameterizedObject`.
|-----------|------|---------|-------------|-------------|
| *(none)* | - | - | - | Base interface has no parameters |
**Timing Behavior**: For value providers, `start()` is typically not called because instances can be embedded in closures. Value providers consider the first call to `produce_value()` as the start of their internal time reference. The `start()` method only resets the time origin if the provider was already started previously (i.e., `self.start_time` is not nil).
**Factory**: N/A (base interface)
### StaticValueProvider
@ -141,6 +145,8 @@ Generates oscillating values using various waveforms. Inherits from `ValueProvid
- `8` (ELASTIC) - Spring-like overshoot and oscillation
- `9` (BOUNCE) - Ball-like bouncing with decreasing amplitude
**Timing Behavior**: The `start_time` is initialized on the first call to `produce_value()`. The `start()` method only resets the time origin if the oscillator was already started previously (i.e., `self.start_time` is not nil).
**Factories**: `animation.ramp(engine)`, `animation.sawtooth(engine)`, `animation.linear(engine)`, `animation.triangle(engine)`, `animation.smooth(engine)`, `animation.sine_osc(engine)`, `animation.cosine_osc(engine)`, `animation.square(engine)`, `animation.ease_in(engine)`, `animation.ease_out(engine)`, `animation.elastic(engine)`, `animation.bounce(engine)`, `animation.oscillator_value(engine)`
**See Also**: [Oscillation Patterns](OSCILLATION_PATTERNS.md) - Visual examples and usage patterns for oscillation waveforms
@ -163,14 +169,14 @@ The ClosureValueProvider includes built-in mathematical helper methods that can
| Method | Description | Parameters | Return Type | Example |
|--------|-------------|------------|-------------|---------|
| `min(a, b, ...)` | Minimum of two or more values | `a, b, *args: number` | `number` | `self.min(5, 3, 8)` → `3` |
| `max(a, b, ...)` | Maximum of two or more values | `a, b, *args: number` | `number` | `self.max(5, 3, 8)` → `8` |
| `abs(x)` | Absolute value | `x: number` | `number` | `self.abs(-5)` → `5` |
| `round(x)` | Round to nearest integer | `x: number` | `int` | `self.round(3.7)` → `4` |
| `sqrt(x)` | Square root with integer handling | `x: number` | `number` | `self.sqrt(64)` → `128` (for 0-255 range) |
| `scale(v, from_min, from_max, to_min, to_max)` | Scale value between ranges | `v, from_min, from_max, to_min, to_max: number` | `int` | `self.scale(50, 0, 100, 0, 255)` → `127` |
| `sin(angle)` | Sine function (0-255 input range) | `angle: number` | `int` | `self.sin(64)` → `255` (90°) |
| `cos(angle)` | Cosine function (0-255 input range) | `angle: number` | `int` | `self.cos(0)` → `-255` (matches oscillator behavior) |
| `min(a, b, ...)` | Minimum of two or more values | `a, b, *args: number` | `number` | `animation._math.min(5, 3, 8)` → `3` |
| `max(a, b, ...)` | Maximum of two or more values | `a, b, *args: number` | `number` | `animation._math.max(5, 3, 8)` → `8` |
| `abs(x)` | Absolute value | `x: number` | `number` | `animation._math.abs(-5)` → `5` |
| `round(x)` | Round to nearest integer | `x: number` | `int` | `animation._math.round(3.7)` → `4` |
| `sqrt(x)` | Square root with integer handling | `x: number` | `number` | `animation._math.sqrt(64)` → `128` (for 0-255 range) |
| `scale(v, from_min, from_max, to_min, to_max)` | Scale value between ranges | `v, from_min, from_max, to_min, to_max: number` | `int` | `animation._math.scale(50, 0, 100, 0, 255)` → `127` |
| `sin(angle)` | Sine function (0-255 input range) | `angle: number` | `int` | `animation._math.sin(64)` → `255` (90°) |
| `cos(angle)` | Cosine function (0-255 input range) | `angle: number` | `int` | `animation._math.cos(0)` → `-255` (matches oscillator behavior) |
**Mathematical Method Notes:**

View File

@ -43,10 +43,8 @@ class MyAnimation : animation.animation
return false
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Use virtual parameter access - automatically resolves ValueProviders
var param1 = self.my_param1
@ -277,6 +275,9 @@ def render(frame, time_ms)
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Get frame dimensions
var width = frame.width
var height = frame.height # Usually 1 for LED strips
@ -372,7 +373,9 @@ class BeaconAnimation : animation.animation
return false
end
# Use engine time if not provided
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
end
@ -538,7 +541,7 @@ anim.pos = 5
anim.beacon_size = 3
engine.add(anim) # Unified method for animations and sequence managers
engine.start()
engine.run()
# Let it run for a few seconds
tasmota.delay(3000)

View File

@ -75,7 +75,6 @@ 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:**
@ -536,10 +535,10 @@ test.opacity = min(255, max(50, scale(sqrt(strip_len), 0, 16, 100, 255)))
- **Integer Optimization**: `sqrt()` function automatically handles integer scaling for 0-255 range values
- **Trigonometric Range**: `sin()` and `cos()` use 0-255 input range (mapped to 0-360°) and return -255 to 255 output range
- **Automatic Detection**: Mathematical functions are automatically detected at transpile time using dynamic introspection
- **Closure Context**: In computed parameters, mathematical functions are called as `self.<function>()` in the generated closure context
- **Closure Context**: In computed parameters, mathematical functions are called as `animation._math.<function>()` in the generated closure context
**How It Works:**
When the DSL detects arithmetic expressions containing value providers, variable references, or mathematical functions, it automatically creates closure functions that capture the computation. These closures are called with `(self, param_name, time_ms)` parameters, allowing the computation to be re-evaluated dynamically as needed. Mathematical functions are automatically prefixed with `self.` in the closure context to access the ClosureValueProvider's mathematical methods.
When the DSL detects arithmetic expressions containing value providers, variable references, or mathematical functions, it automatically creates closure functions that capture the computation. These closures are called with `(self, param_name, time_ms)` parameters, allowing the computation to be re-evaluated dynamically as needed. Mathematical functions are automatically prefixed with `animation._math.` in the closure context to access the ClosureValueProvider's mathematical methods.
**User Functions in Computed Parameters:**
User-defined functions can also be used in computed parameter expressions, providing powerful custom effects. User functions must be called with the `user.` prefix:
@ -785,31 +784,32 @@ sequence cylon_eye {
}
```
#### Reset and Restart Statements
#### Restart Statements
Reset and restart statements allow you to reset value providers and animations to their initial state during sequence execution:
Restart statements allow you to restart value providers and animations from their initial state during sequence execution:
```berry
reset value_provider_name # Reset value provider to initial state
restart value_provider_name # Restart value provider from beginning
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 value providers (oscillators, color cycles, etc.) from their initial state
- Restarts animations from their beginning state
- Calls the `start()` method on the animation
- Useful for restarting complex animations or synchronizing multiple animations
- Calls the `start()` method on the value provider or animation, which resets the time origin only if the object was already started previously
- Useful for synchronizing oscillators, restarting color cycles, or restarting complex animations
**Timing Behavior:**
- The `start()` method only resets the time origin if `self.start_time` is not nil (i.e., the object was already started)
- For fresh objects, the first call to `update()`, `render()`, or `produce_value()` initializes the time reference
- This prevents premature time initialization and ensures proper timing behavior
**Examples:**
```berry
# Reset oscillators for synchronized movement
# Restart oscillators for synchronized movement
sequence sync_demo {
play wave_anim for 3s
reset position_osc # Reset oscillator to start position
restart position_osc # Restart oscillator time origin
play wave_anim for 3s
}
@ -980,7 +980,7 @@ animation.register_user_function('pulse_effect', pulse_effect_template)
- Templates don't return values - they add animations directly to the engine
- Multiple `run` statements in templates add multiple animations
- Templates can be called multiple times to create multiple instances
- `engine.start()` is automatically called when templates are used at the top level
- `engine.run()` is automatically called when templates are used at the top level
## Execution Statements
@ -1293,13 +1293,12 @@ property_assignment = identifier "." identifier "=" expression ;
(* Sequences *)
sequence = "sequence" identifier [ "repeat" ( expression "times" | "forever" ) ] "{" sequence_body "}" ;
sequence_body = { sequence_statement } ;
sequence_statement = play_stmt | wait_stmt | repeat_stmt | sequence_assignment | reset_stmt | restart_stmt ;
sequence_statement = play_stmt | wait_stmt | repeat_stmt | sequence_assignment | restart_stmt ;
play_stmt = "play" identifier [ "for" time_expression ] ;
wait_stmt = "wait" time_expression ;
repeat_stmt = "repeat" ( expression "times" | "forever" ) "{" sequence_body "}" ;
sequence_assignment = identifier "." identifier "=" expression ;
reset_stmt = "reset" identifier ;
restart_stmt = "restart" identifier ;
(* Templates *)

View File

@ -255,9 +255,9 @@ import "user_functions"
var test_ = animation.solid(engine)
test_.color = 0xFF0000FF
test_.opacity = animation.create_closure_value(engine,
def (self) return animation.get_user_function('rand_demo')(self.engine) end)
def (engine) return animation.get_user_function('rand_demo')(engine) end)
engine.add(test_)
engine.start()
engine.run()
```
## Advanced DSL Features
@ -291,7 +291,7 @@ def pulse_effect(engine, color, speed)
pulse_.color = color
pulse_.period = speed
engine.add(pulse_)
engine.start()
engine.run()
end
animation.register_user_function("pulse_effect", pulse_effect)
@ -347,7 +347,7 @@ def comet_chase(engine, trail_color, bg_color, chase_speed)
comet_.speed = chase_speed
engine.add(background_)
engine.add(comet_)
engine.start()
engine.run()
end
animation.register_user_function("comet_chase", comet_chase)

View File

@ -173,7 +173,7 @@ sequence demo {
run demo
```
### 11. Reset and Restart in Sequences
### 11. Restart in Sequences
```berry
# Create oscillator and animation
set wave_osc = triangle(min_value=0, max_value=29, period=4s)
@ -181,9 +181,9 @@ 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
restart wave_osc # Restart oscillator time origin (if already started)
play wave for 3s # Wave starts from beginning again
restart wave # Restart animation from initial state
restart wave # Restart animation time origin (if already started)
play wave for 3s
}
run sync_demo
@ -199,7 +199,7 @@ sequence breathing_cycle {
play pulse for 500ms
pulse.opacity = brightness # Apply breathing effect
wait 200ms
pulse.opacity = 255 # Reset to full brightness
pulse.opacity = 255 # Return to full brightness
}
}
run breathing_cycle

View File

@ -53,10 +53,10 @@ transpile()
│ │ ├── process_play_statement_fluent()
│ │ ├── process_wait_statement_fluent()
│ │ ├── process_log_statement_fluent()
│ │ ├── process_reset_restart_statement_fluent()
│ │ ├── process_restart_statement_fluent()
│ │ └── process_sequence_assignment_fluent()
│ ├── process_import() (direct Berry import generation)
│ ├── process_run() (collect for single engine.start())
│ ├── process_run() (collect for single engine.run())
│ └── process_property_assignment()
└── generate_engine_start() (single call for all run statements)
```
@ -141,7 +141,7 @@ process_value(context)
│ │ └── process_primary_expression(context, is_top_level, raw_mode)
│ │ ├── Parenthesized expression → recursive call
│ │ ├── Function call handling:
│ │ │ ├── Raw mode: mathematical functions → self.method()
│ │ │ ├── Raw mode: mathematical functions → animation._math.method()
│ │ │ ├── Raw mode: template calls → template_func(self.engine, ...)
│ │ │ ├── Regular mode: process_function_call() or process_nested_function_call()
│ │ │ └── Simple function detection → _is_simple_function_call()
@ -199,11 +199,11 @@ is_computed_expression_string(expr_str)
create_computation_closure_from_string(expr_str)
├── transform_expression_for_closure()
│ ├── Sequential step 1: Transform mathematical functions → self.method()
│ ├── Sequential step 1: Transform mathematical functions → animation._math.method()
│ │ ├── Use dynamic introspection with is_math_method()
│ │ ├── Check for existing "self." prefix
│ │ ├── Check for existing "self." prefix /// TODO NOT SURE IT STILL EXISTS
│ │ └── Only transform if not already prefixed
│ ├── Sequential step 2: Transform user variables → self.resolve(var_)
│ ├── Sequential step 2: Transform user variables → animation.resolve(var_)
│ │ ├── Find variables ending with _
│ │ ├── Check for existing resolve() calls
│ │ ├── Avoid double-wrapping
@ -290,7 +290,7 @@ _validate_value_provider_reference(object_name, context)
├── Check if symbol exists using validate_symbol_reference()
├── Check symbol_table markers for type information
├── Validate instance types using isinstance()
├── Ensure only value providers/animations can be reset/restarted
├── Ensure only value providers/animations can be restarted
└── Provide detailed error messages for invalid types
```
@ -331,14 +331,14 @@ Dynamic expressions are wrapped in closures with **mathematical function support
# DSL: animation.opacity = strip_length() / 2 + 50
# Generated:
animation.opacity = animation.create_closure_value(engine,
def (self) return self.resolve(strip_length_(engine)) / 2 + 50 end)
def (self) return animation.resolve(strip_length_(engine)) / 2 + 50 end)
# DSL: animation.opacity = max(100, min(255, user.rand_demo() + 50))
# Generated:
animation.opacity = animation.create_closure_value(engine,
def (self) return self.max(100, self.min(255, animation.get_user_function('rand_demo')(self.engine) + 50)) end)
def (self) return animation._math.max(100, animation._math.min(255, animation.get_user_function('rand_demo')(engine) + 50)) end)
# Mathematical functions are automatically detected and prefixed with self.
# Mathematical functions are automatically detected and prefixed with animation._math.
# User functions are wrapped with animation.get_user_function() calls
```

View File

@ -78,6 +78,12 @@ except .. as e, msg
end
```
**Timing Behavior Note:**
The framework has updated timing behavior where:
- The `start()` method only resets the time origin if the animation/value provider was already started previously
- The first actual rendering tick occurs in `update()`, `render()`, or `produce_value()` methods
- This ensures proper timing initialization and prevents premature time reference setting
**Common Solutions:**
1. **Missing Strip Declaration:**
@ -761,7 +767,7 @@ var engine = animation.create_engine(strip)
var red_anim = animation.solid(engine)
red_anim.color = 0xFFFF0000
engine.add(red_anim)
engine.start()
engine.run()
# If basic strip works but animation doesn't, check framework setup
```
@ -831,7 +837,7 @@ var engine = animation.create_engine(strip, true) # debug=true
var anim = animation.solid(engine)
anim.color = 0xFFFF0000
engine.add(anim)
engine.start()
engine.run()
```
### Step-by-Step Testing
@ -856,7 +862,7 @@ engine.add(anim)
print("Animation count:", engine.size())
print("5. Starting engine...")
engine.start()
engine.run()
print("Engine active:", engine.is_active())
```

View File

@ -490,7 +490,7 @@ When you use user functions in computed parameters:
1. **Automatic Detection**: The transpiler automatically detects user functions in expressions
2. **Single Closure**: The entire expression is wrapped in a single efficient closure
3. **Engine Access**: User functions receive `self.engine` in the closure context
3. **Engine Access**: User functions receive `engine` in the closure context
4. **Mixed Operations**: User functions work seamlessly with mathematical functions and arithmetic
**Generated Code Example:**
@ -502,8 +502,8 @@ animation.opacity = max(100, user.breathing(red, 2000))
**Transpiles to:**
```berry
animation.opacity = animation.create_closure_value(engine,
def (self, param_name, time_ms)
return (self.max(100, animation.get_user_function('breathing')(self.engine, 0xFFFF0000, 2000)))
def (engine, param_name, time_ms)
return (animation._math.max(100, animation.get_user_function('breathing')(engine, 0xFFFF0000, 2000)))
end)
```

View File

@ -57,6 +57,10 @@ end
# Import core framework components
# These provide the fundamental architecture for the animation system
# Mathematical functions for use in closures and throughout the framework
import "core/math_functions" as math_functions
register_to_animation(math_functions)
# Base class for parameter management - shared by Animation and ValueProvider
import "core/parameterized_object" as parameterized_object
register_to_animation(parameterized_object)

View File

@ -40,10 +40,8 @@ class BeaconAnimation : animation.animation
return false
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var pixel_size = frame.width
# Use virtual parameter access - automatically resolves ValueProviders

View File

@ -84,6 +84,7 @@ class BounceAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "bounce_speed"
# Update velocity if speed changed
var pixels_per_second = tasmota.scale_uint(value, 0, 255, 0, 20)

View File

@ -39,6 +39,7 @@ class BreatheAnimation : animation.animation
# Handle parameter changes - propagate to internal breathe provider
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
# Propagate relevant parameters to the breathe provider
if name == "base_color"
self.breathe_provider.base_color = value

View File

@ -36,6 +36,7 @@ class CometAnimation : animation.animation
# Handle parameter changes - reset position when direction changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "direction"
# Reset position when direction changes
var strip_length = self.engine.get_strip_length()

View File

@ -42,10 +42,8 @@ class CrenelPositionAnimation : animation.animation
return false
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var pixel_size = frame.width

View File

@ -52,12 +52,6 @@ class FireAnimation : animation.animation
end
end
# Handle parameter changes
def on_param_changed(name, value)
# No special handling needed - parameters are accessed via virtual members
# The default fire palette is set up by factory methods when needed
end
# Simple pseudo-random number generator
# Uses a linear congruential generator for consistent results
def _random()
@ -226,6 +220,9 @@ class FireAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()
# Render each pixel with its current color

View File

@ -42,9 +42,8 @@ class GradientAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
# No special handling needed for most parameters
# The virtual parameter system handles storage and validation
super(self).on_param_changed(name, value)
# TODO maybe be more specific on attribute name
# Handle strip length changes from engine
var current_strip_length = self.engine.get_strip_length()
if size(self.current_colors) != current_strip_length
@ -201,6 +200,9 @@ class GradientAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()
var i = 0
while i < strip_length && i < frame.width

View File

@ -87,6 +87,10 @@ class JitterAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
# Cache parameter values for performance
var jitter_frequency = self.jitter_frequency
var source_animation = self.source_animation
@ -232,10 +236,13 @@ class JitterAnimation : animation.animation
# Render jitter to frame buffer
def render(frame, time_ms)
if frame == nil
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var i = 0
while i < current_strip_length

View File

@ -116,6 +116,7 @@ class NoiseAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "seed"
self._init_noise_table()
end
@ -236,6 +237,9 @@ class NoiseAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()
var i = 0
while i < strip_length

View File

@ -83,6 +83,9 @@ class PalettePatternAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
@ -102,10 +105,8 @@ class PalettePatternAnimation : animation.animation
return false
end
# Use provided time or default to engine time
if time_ms == nil
time_ms = self.engine.time_ms
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Get current parameter values (cached for performance)
var color_source = self.get_param('color_source') # use get_param to avoid resolving of color_provider
@ -139,14 +140,13 @@ class PalettePatternAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "pattern_func" || name == "color_source"
# Reinitialize value buffer when pattern or color source changes
self._initialize_value_buffer()
end
end
# String representation of the animation
def tostring()
var strip_length = self.engine.get_strip_length()

View File

@ -85,6 +85,7 @@ class PlasmaAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "color" && value == nil
# Reset to default rainbow palette when color is set to nil
var rainbow_provider = animation.rich_palette(self.engine)
@ -190,6 +191,9 @@ class PlasmaAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()
var i = 0
while i < strip_length

View File

@ -20,7 +20,7 @@ class RichPaletteAnimation : animation.animation
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.SINE},
"brightness": {"min": 0, "max": 255, "default": 255},
"range_min": {"default": 0},
"range_max": {"default": 100}
"range_max": {"default": 255}
}
# Initialize a new RichPaletteAnimation
@ -45,6 +45,7 @@ class RichPaletteAnimation : animation.animation
# @param name: string - Name of the parameter that changed
# @param value: any - New value of the parameter
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
# Forward rich palette parameters to internal color provider
if name == "palette" || name == "cycle_period" || name == "transition_type" ||
name == "brightness" || name == "range_min" || name == "range_max"

View File

@ -48,12 +48,6 @@ class ScaleAnimation : animation.animation
end
end
# Handle parameter changes
def on_param_changed(name, value)
# No special handling needed for most parameters
# Buffers are managed through engine strip length changes
end
# Start/restart the animation
def start(time_ms)
# Call parent start first (handles ValueProvider propagation)
@ -73,6 +67,10 @@ class ScaleAnimation : animation.animation
# Update animation state
def update(time_ms)
if !super(self).update(time_ms)
return false
end
# Cache parameter values for performance
var current_scale_speed = self.scale_speed
var current_scale_mode = self.scale_mode
@ -240,6 +238,9 @@ class ScaleAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var i = 0
while i < current_strip_length

View File

@ -45,6 +45,7 @@ class ShiftAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
# Re-initialize buffers if strip length might have changed
if name == "source_animation"
self._initialize_buffers()
@ -53,7 +54,9 @@ class ShiftAnimation : animation.animation
# Update animation state
def update(time_ms)
super(self).update(time_ms)
if !super(self).update(time_ms)
return false
end
# Cache parameter values for performance
var current_shift_speed = self.shift_speed
@ -152,6 +155,9 @@ class ShiftAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var i = 0
while i < current_strip_length

View File

@ -92,7 +92,9 @@ class SparkleAnimation : animation.animation
# Update animation state
def update(time_ms)
super(self).update(time_ms)
if !super(self).update(time_ms)
return false
end
# Update at approximately 30 FPS
var update_interval = 33 # ~30 FPS
@ -199,6 +201,9 @@ class SparkleAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var i = 0
while i < current_strip_length

View File

@ -61,6 +61,7 @@ class TwinkleAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "twinkle_speed"
# Handle twinkle_speed - can be Hz (1-20) or period in ms (50-5000)
if value >= 50 # Assume it's period in milliseconds
@ -103,10 +104,8 @@ class TwinkleAnimation : animation.animation
return false
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access parameters via virtual members
var twinkle_speed = self.twinkle_speed
@ -199,10 +198,8 @@ class TwinkleAnimation : animation.animation
return false
end
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()

View File

@ -87,6 +87,7 @@ class WaveAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "wave_type"
self._init_wave_table() # Regenerate wave table when wave type changes
end
@ -199,6 +200,9 @@ class WaveAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()
var i = 0
while i < strip_length

View File

@ -9,14 +9,11 @@
class Animation : animation.parameterized_object
# Non-parameter instance variables only
var start_time # Time when animation started (ms) (int)
var current_time # Current animation time (ms) (int)
var opacity_frame # Frame buffer for opacity animation rendering
# Parameter definitions
static var PARAMS = {
"name": {"type": "string", "default": "animation"}, # Optional name for the animation
"is_running": {"type": "bool", "default": false}, # Whether the animation is active
"priority": {"min": 0, "default": 10}, # Rendering priority (higher = on top, 0-255)
"duration": {"min": 0, "default": 0}, # Animation duration in ms (0 = infinite)
"loop": {"type": "bool", "default": false}, # Whether to loop when duration is reached
@ -31,60 +28,7 @@ class Animation : animation.parameterized_object
# Initialize parameter system with engine
super(self).init(engine)
# Initialize non-parameter instance variables
self.start_time = 0
self.current_time = 0
end
# Start/restart the animation (make it active and reset timing)
#
# @param start_time: int - Optional start time in milliseconds
# @return self for method chaining
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
self.start_time = actual_start_time
self.current_time = self.start_time
# Start/restart all value providers in parameters
self._start_value_providers(actual_start_time)
return self
end
# Helper method to start/restart all value providers in parameters
#
# @param time_ms: int - Time to pass to value provider start methods
def _start_value_providers(time_ms)
# Iterate through all parameter values
for param_value : self.values
# 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)
param_value.start(time_ms)
end
end
end
# Handle parameter changes - specifically for is_running to control start/stop
#
# @param name: string - Parameter name that changed
# @param value: any - New parameter value
def on_param_changed(name, value)
if name == "is_running"
if value == true
# Start the animation (but avoid infinite loop by not setting is_running again)
var actual_start_time = self.engine.time_ms
self.start_time = actual_start_time
self.current_time = self.start_time
# Start/restart all value providers in parameters
self._start_value_providers(actual_start_time)
# elif value == false
# Stop the animation - just set the internal state
# (is_running is already set to false by the parameter system)
end
end
# Initialize non-parameter instance variables (none currently)
end
# Update animation state based on current time
@ -93,14 +37,15 @@ class Animation : animation.parameterized_object
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access is_running via virtual member
var current_is_running = self.is_running
if !current_is_running
return false
end
self.current_time = time_ms
var elapsed = self.current_time - self.start_time
var elapsed = time_ms - self.start_time
# Access parameters via virtual members
var current_duration = self.duration
@ -131,17 +76,16 @@ class Animation : animation.parameterized_object
# @param time_ms: int - Current time in milliseconds
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
# auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access is_running via virtual member
var current_is_running = self.is_running
if !current_is_running || frame == nil
return false
end
# Use engine time if not provided
time_ms = (time_ms != nil) ? time_ms : self.engine.time_ms
# Update animation state
self.update(time_ms)
self.update(time_ms) # TODO IS UPDATE NOT ALREADY CALLED BY ENGINE?
# Access parameters via virtual members (auto-resolves ValueProviders)
var current_color = self.color
@ -159,6 +103,7 @@ class Animation : animation.parameterized_object
# @param frame: FrameBuffer - The frame buffer to render to
# @param time_ms: int - Current time in milliseconds
def post_render(frame, time_ms)
# no need to auto-fix time_ms and start_time
# Handle opacity - can be number, frame buffer, or animation
var current_opacity = self.opacity
self._apply_opacity(frame, current_opacity, time_ms)

View File

@ -42,10 +42,10 @@ class AnimationEngine
self.render_needed = false
end
# Start the animation engine
# Run the animation engine
#
# @return self for method chaining
def start()
def run()
if !self.is_running
var now = tasmota.millis()
self.is_running = true

View File

@ -0,0 +1,120 @@
# Mathematical Functions for Animation Framework
#
# This module provides mathematical functions that can be used in closures
# and throughout the animation framework. These functions are optimized for
# the animation use case and handle integer ranges appropriately.
# This class contains only static functions
class AnimationMath
# Minimum of two or more values
#
# @param *args: number - Values to compare
# @return number - Minimum value
#@ solidify:min,weak
static def min(*args)
import math
return call(math.min, args)
end
# Maximum of two or more values
#
# @param *args: number - Values to compare
# @return number - Maximum value
#@ solidify:max,weak
static def max(*args)
import math
return call(math.max, args)
end
# Absolute value
#
# @param x: number - Input value
# @return number - Absolute value
#@ solidify:abs,weak
static def abs(x)
import math
return math.abs(x)
end
# Round to nearest integer
#
# @param x: number - Input value
# @return int - Rounded value
#@ solidify:round,weak
static def round(x)
import math
return int(math.round(x))
end
# Square root with integer handling
# For integers, treats 1.0 as 255 (full scale)
#
# @param x: number - Input value
# @return number - Square root
#@ solidify:sqrt,weak
static def sqrt(x)
import math
# If x is an integer in 0-255 range, scale to 0-1 for sqrt, then back
if type(x) == 'int' && x >= 0 && x <= 255
var normalized = x / 255.0
return int(math.sqrt(normalized) * 255)
else
return math.sqrt(x)
end
end
# Scale a value from one range to another using tasmota.scale_int
#
# @param v: number - Value to scale
# @param from_min: number - Source range minimum
# @param from_max: number - Source range maximum
# @param to_min: number - Target range minimum
# @param to_max: number - Target range maximum
# @return int - Scaled value
#@ solidify:scale,weak
static def scale(v, from_min, from_max, to_min, to_max)
return tasmota.scale_int(v, from_min, from_max, to_min, to_max)
end
# Sine function using tasmota.sine_int (works on integers)
# Input angle is in 0-255 range (mapped to 0-360 degrees)
# Output is in -255 to 255 range (mapped from -1.0 to 1.0)
#
# @param angle: number - Angle in 0-255 range (0-360 degrees)
# @return int - Sine value in -255 to 255 range
#@ solidify:sin,weak
static def sin(angle)
# Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)
var tasmota_angle = tasmota.scale_int(angle, 0, 255, 0, 32767)
# Get sine value from -4096 to 4096 (representing -1.0 to 1.0)
var sine_val = tasmota.sine_int(tasmota_angle)
# Map from -4096..4096 to -255..255 for integer output
return tasmota.scale_int(sine_val, -4096, 4096, -255, 255)
end
# Cosine function using tasmota.sine_int with phase shift
# Input angle is in 0-255 range (mapped to 0-360 degrees)
# Output is in -255 to 255 range (mapped from -1.0 to 1.0)
# Note: This matches the oscillator COSINE behavior (starts at minimum, not maximum)
#
# @param angle: number - Angle in 0-255 range (0-360 degrees)
# @return int - Cosine value in -255 to 255 range
#@ solidify:cos,weak
static def cos(angle)
# Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)
var tasmota_angle = tasmota.scale_int(angle, 0, 255, 0, 32767)
# Get cosine value by shifting sine by -90 degrees (matches oscillator behavior)
var cosine_val = tasmota.sine_int(tasmota_angle - 8192)
# Map from -4096..4096 to -255..255 for integer output
return tasmota.scale_int(cosine_val, -4096, 4096, -255, 255)
end
end
# Export only the _math namespace containing all math functions
return {
'_math': AnimationMath
}

View File

@ -11,9 +11,12 @@
class ParameterizedObject
var values # Map storing all parameter values
var engine # Reference to the animation engine
var start_time # Time when object started (ms) (int), value is set at first call to update() or render()
# Static parameter definitions - should be overridden by subclasses
static var PARAMS = {}
static var PARAMS = {
"is_running": {"type": "bool", "default": false} # Whether the object is active
}
# Initialize parameter system
#
@ -348,10 +351,47 @@ class ParameterizedObject
return self._resolve_parameter_value(param_name, time_ms)
end
# Start the object - placeholder for future implementation
# Helper function to make sure both self.start_time and time_ms are valid
#
# If time_ms is nil, replace with time_ms from engine
# Then initialize the value for self.start_time if not set already
#
# @param time_ms: int or nil - Current time in milliseconds
# @return time_ms: int (guaranteed)
def _fix_time_ms(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
end
if time_ms == nil
raise "value_error", "engine.time_ms should not be 'nil'"
end
if self.start_time == nil
self.start_time = time_ms
end
return time_ms
end
# Start the object - base implementation
#
# `start(time_ms)` is called whenever an animation is about to be run
# by the animation engine directly or via a sequence manager.
# For value providers, start is typically not called because instances
# can be embedded in closures. So value providers must consider the first
# call to `produce_value()` as a start of their internal time reference.
# @param start_time: int - Optional start time in milliseconds
# @return self for method chaining
def start(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
end
if time_ms == nil
raise "value_error", "engine.time_ms should not be 'nil'"
end
if self.start_time != nil # reset time only if it was already started
self.start_time = time_ms
end
# Set is_running directly in values map to avoid infinite loop
self.values["is_running"] = true
return self
end
@ -361,7 +401,16 @@ class ParameterizedObject
# @param name: string - Parameter name
# @param value: any - New parameter value
def on_param_changed(name, value)
# Default implementation does nothing
if name == "is_running"
if value == true
# Start the object (but avoid infinite loop by not setting is_running again)
# Call start method to handle start_time
self.start(nil)
elif value == false
# Stop the object - just set the internal state
# (is_running is already set to false by the parameter system)
end
end
end
# Equality operator for object identity comparison

View File

@ -197,7 +197,21 @@ class SequenceManager
if step["type"] == "play"
var anim = step["animation"]
# Check if animation is already in the engine (avoid duplicate adds)
var animations = self.engine.get_animations()
var already_added = false
for existing_anim : animations
if existing_anim == anim
already_added = true
break
end
end
if !already_added
self.engine.add(anim)
end
# Always restart the animation to ensure proper timing
anim.start(current_time)
elif step["type"] == "wait"
@ -292,6 +306,25 @@ class SequenceManager
end
end
# CRITICAL FIX: Handle the case where the next step is the SAME animation
# This prevents removing and re-adding the same animation, which causes black frames
var next_step = nil
var is_same_animation = false
if self.step_index < size(self.steps)
next_step = self.steps[self.step_index]
if next_step["type"] == "play" && previous_anim != nil
is_same_animation = (next_step["animation"] == previous_anim)
end
end
if is_same_animation
# Same animation continuing - don't remove/re-add, but DO restart for timing sync
self.step_start_time = current_time
# CRITICAL: Still need to restart the animation to sync with sequence timing
previous_anim.start(current_time)
else
# Different animation or no next animation
# Start the next animation BEFORE removing the previous one
if self.step_index < size(self.steps)
self.execute_current_step(current_time)
@ -301,6 +334,7 @@ class SequenceManager
if previous_anim != nil
self.engine.remove(previous_anim)
end
end
# Handle completion
if self.step_index >= size(self.steps)

View File

@ -1,8 +1,6 @@
# User-Defined Functions Registry for Berry Animation Framework
# This module manages external Berry functions that can be called from DSL code
#@ solidify:animation_user_functions,weak
# Register a Berry function for DSL use
def register_user_function(name, func)
animation._user_functions[name] = func

View File

@ -21,14 +21,14 @@ class SimpleDSLTranspiler
var pos # Current token position
var output # Generated Berry code lines
var errors # Compilation errors
var run_statements # Collect all run statements for single engine.start()
var run_statements # Collect all run statements for single engine.run()
var first_statement # Track if we're processing the first statement
var strip_initialized # Track if strip was initialized
var sequence_names # Track which names are sequences
var symbol_table # Track created objects: name -> instance
var indent_level # Track current indentation level for nested sequences
var template_definitions # Track template definitions: name -> {params, body}
var has_template_calls # Track if we have template calls to trigger engine.start()
var has_template_calls # Track if we have template calls to trigger engine.run()
# Static color mapping for named colors (helps with solidification)
static var named_colors = {
@ -75,9 +75,9 @@ class SimpleDSLTranspiler
# Don't check for existence during transpilation - trust that function will be available at runtime
# User functions use positional parameters with engine as first argument
# In closure context, use self.engine to access the engine from the ClosureValueProvider
# In closure context, use engine parameter directly
var args = self.process_function_arguments(true)
var full_args = args != "" ? f"self.engine, {args}" : "self.engine"
var full_args = args != "" ? f"engine, {args}" : "engine"
return f"animation.get_user_function('{func_name}')({full_args})"
else
self.error("User functions must be called with parentheses: user.function_name()")
@ -96,8 +96,8 @@ class SimpleDSLTranspiler
self.process_statement()
end
# Generate single engine.start() call after all run statements
self.generate_engine_start()
# Generate single engine.run() call after all run statements
self.generate_engine_run()
return size(self.errors) == 0 ? self.join_output() : nil
except .. as e, msg
@ -784,8 +784,12 @@ class SimpleDSLTranspiler
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 == "restart"
self.process_restart_statement_fluent()
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "reset"
self.error("'reset' command is no longer supported. Use 'restart' instead.")
self.skip_statement()
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "repeat"
self.next() # skip 'repeat'
@ -827,12 +831,12 @@ class SimpleDSLTranspiler
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.error(f"Unknown command '{tok.value}' in sequence. Valid sequence commands are: play, wait, repeat, 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.error(f"Invalid statement in sequence. Expected: play, wait, repeat, restart, log, or property assignments")
self.skip_statement()
end
end
@ -981,10 +985,10 @@ class SimpleDSLTranspiler
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'
# Helper method to process restart statement using fluent style
def process_restart_statement_fluent()
var keyword = self.current().value # "restart"
self.next() # skip 'restart'
# Expect the value provider identifier
var val_name = self.expect_identifier()
@ -1076,7 +1080,7 @@ class SimpleDSLTranspiler
var inline_comment = self.collect_inline_comment()
self.add(f"{object_name}_template({full_args}){inline_comment}")
# Track that we have template calls to trigger engine.start()
# Track that we have template calls to trigger engine.run()
self.has_template_calls = true
else
self.error(f"Standalone function calls are only supported for templates. '{object_name}' is not a template.")
@ -1274,7 +1278,7 @@ class SimpleDSLTranspiler
# Check if this is a mathematical function
if self.is_math_method(func_name)
var args = self.process_function_arguments(true)
return f"self.{func_name}({args})"
return f"animation._math.{func_name}({args})"
end
# Special case for log function in expressions
@ -1286,7 +1290,7 @@ class SimpleDSLTranspiler
# Check if this is a template call
if self.template_definitions.contains(func_name)
var args = self.process_function_arguments(true)
var full_args = args != "" ? f"self.engine, {args}" : "self.engine"
var full_args = args != "" ? f"engine, {args}" : "engine"
return f"{func_name}_template({full_args})"
end
@ -1379,7 +1383,7 @@ class SimpleDSLTranspiler
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}')"
return f"animation.resolve({object_ref}, '{property_name}')"
end
end
@ -1481,7 +1485,7 @@ class SimpleDSLTranspiler
transformed_expr = string.replace(transformed_expr, " ", " ")
end
var closure_code = f"def (self) return {transformed_expr} end"
var closure_code = f"def (engine) return {transformed_expr} end"
# Return a closure value provider instance
return f"animation.create_closure_value(engine, {closure_code})"
@ -1506,7 +1510,7 @@ class SimpleDSLTranspiler
right_expr = string.replace(right_expr, " ", " ")
end
var closure_code = f"def (self) return {left_expr} {op} {right_expr} end"
var closure_code = f"def (engine) return {left_expr} {op} {right_expr} end"
# Return a closure value provider instance
return f"animation.create_closure_value(engine, {closure_code})"
@ -1526,48 +1530,7 @@ class SimpleDSLTranspiler
var result = expr_str
var pos = 0
# First pass: Transform mathematical function calls to self.method() calls
# Use a simple pattern-based approach that works with the existing logic
var search_pos = 0
while true
var paren_pos = string.find(result, "(", search_pos)
if paren_pos < 0
break
end
# Find the function name before the parenthesis
var func_start = paren_pos - 1
while func_start >= 0 && self.is_identifier_char(result[func_start])
func_start -= 1
end
func_start += 1
if func_start < paren_pos
var func_name = result[func_start..paren_pos-1]
# Check if this is a mathematical method using dynamic introspection
if self.is_math_method(func_name)
# Check if it's not already prefixed with "self."
var prefix_start = func_start >= 5 ? func_start - 5 : 0
var prefix = result[prefix_start..func_start-1]
if string.find(prefix, "self.") < 0
# Replace the function call with self.method()
var before = func_start > 0 ? result[0..func_start-1] : ""
var after = result[func_start..]
result = before + "self." + after
search_pos = func_start + 5 + size(func_name) # Skip past "self." + func_name
else
search_pos = paren_pos + 1
end
else
search_pos = paren_pos + 1
end
else
search_pos = paren_pos + 1
end
end
# Second pass: Replace all user variables (ending with _) with resolve calls
# Replace all user variables (ending with _) with resolve calls
pos = 0
while pos < size(result)
var underscore_pos = string.find(result, "_", pos)
@ -1581,25 +1544,19 @@ class SimpleDSLTranspiler
start_pos -= 1
end
# Check if this is a user variable (not preceded by "animation." or "self." or already inside a resolve call)
# Check if this is a user variable (not preceded by "animation." or already inside a resolve call)
var is_user_var = true
if start_pos >= 13
var check_start = start_pos >= 13 ? start_pos - 13 : 0
if start_pos >= 18
var check_start = start_pos >= 18 ? start_pos - 18 : 0
var prefix = result[check_start..start_pos-1]
if string.find(prefix, "self.resolve(") >= 0
if string.find(prefix, "animation.resolve(") >= 0
is_user_var = false
end
end
if is_user_var && start_pos >= 10
var check_start = start_pos >= 10 ? start_pos - 10 : 0
var prefix = result[check_start..start_pos-1]
if string.find(prefix, "animation.") >= 0 || string.find(prefix, "self.") >= 0
is_user_var = false
end
elif is_user_var && start_pos >= 5
var check_start = start_pos >= 5 ? start_pos - 5 : 0
var prefix = result[check_start..start_pos-1]
if string.find(prefix, "self.") >= 0
if string.find(prefix, "animation.") >= 0
is_user_var = false
end
end
@ -1612,7 +1569,7 @@ class SimpleDSLTranspiler
var end_pos = underscore_pos + 1
if end_pos >= size(result) || !self.is_identifier_char(result[end_pos])
# Replace the variable with the resolve call
var replacement = f"self.resolve({var_name})"
var replacement = f"animation.resolve({var_name})"
var before = start_pos > 0 ? result[0..start_pos-1] : ""
var after = end_pos < size(result) ? result[end_pos..] : ""
result = before + replacement + after
@ -1633,28 +1590,14 @@ class SimpleDSLTranspiler
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'
end
# Helper method to check if a function name is a mathematical method in ClosureValueProvider
# Helper method to check if a function name is a mathematical function
def is_math_method(func_name)
import introspect
try
# Get the ClosureValueProvider class from the animation module
var closure_provider_class = animation.closure_value
if closure_provider_class == nil
return false
end
# Check if the method exists in the class
var members = introspect.members(closure_provider_class)
for member : members
if member == func_name
# Additional check: make sure it's actually a method (function)
var method = introspect.get(closure_provider_class, func_name)
return introspect.ismethod(method) || type(method) == 'function'
end
end
return false
import introspect
# Check if the function is registered in the animation._math map
return introspect.contains(animation._math, func_name)
except .. as e, msg
# If introspection fails, return false to be safe
# If _math map doesn't exist or access fails, return false to be safe
return false
end
end
@ -1684,7 +1627,7 @@ class SimpleDSLTranspiler
if has_underscore && !has_operators && !has_paren && !has_animation_prefix
# This looks like a simple user variable that might be a ValueProvider
return f"self.resolve({operand})"
return f"animation.resolve({operand})"
else
# For other expressions (literals, animation module calls, complex expressions), use as-is
return operand
@ -1936,7 +1879,7 @@ class SimpleDSLTranspiler
if self.is_math_method(func_name)
# Mathematical functions use positional arguments, not named parameters
var args = self.process_function_arguments(true)
return f"self.{func_name}({args})" # Prefix with self. for closure context
return f"animation._math.{func_name}({args})" # Math functions are under _math namespace
end
# Special case for log function in nested calls
@ -1950,7 +1893,7 @@ class SimpleDSLTranspiler
if self.template_definitions.contains(func_name)
# This is a template call - treat like user function
var args = self.process_function_arguments(true)
var full_args = args != "" ? f"self.engine, {args}" : "self.engine"
var full_args = args != "" ? f"engine, {args}" : "engine"
return f"{func_name}_template({full_args})"
else
# Check if this is a simple function call without named parameters
@ -2324,8 +2267,8 @@ class SimpleDSLTranspiler
return report
end
# Generate single engine.start() call for all run statements
def generate_engine_start()
# Generate single engine.run() call for all run statements
def generate_engine_run()
if size(self.run_statements) == 0 && !self.has_template_calls
return # No run statements or template calls, no need to start engine
end
@ -2340,8 +2283,8 @@ class SimpleDSLTranspiler
self.add(f"engine.add({name}_){comment}")
end
# Single engine.start() call
self.add("engine.start()")
# Single engine.run() call
self.add("engine.run()")
end
# Basic event handler processing
@ -2654,14 +2597,14 @@ class SimpleDSLTranspiler
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.")
self.error(f"'{object_name}' in {context} statement is not a value provider or animation. Only value providers (like oscillators) and animations can be 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.")
self.error(f"'{object_name}' in {context} statement is not a value provider or animation. Only value providers (like oscillators) and animations can be restarted.")
return false
end
end

View File

@ -37,6 +37,7 @@ class BreatheColorProvider : animation.oscillator_value
# Handle parameter changes - no need to sync oscillator min/max since they're fixed
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
# Only handle curve_factor changes for oscillator form
if name == "curve_factor"
# For curve_factor = 1, use pure cosine

View File

@ -30,33 +30,12 @@ class ClosureValueProvider : animation.value_provider
# @param name: string - Parameter name
# @param value: any - New parameter value
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "closure"
self._closure = value
end
end
# Helper method to resolve a value that can be either static or from a value provider
# This is equivalent to 'resolve_param' but with a shorter name
# and available at first dereferencing of method name (hence faster)
#
# @param value: any - Static value, value provider instance, or parameterized object
# @param param_name: string - Parameter name for specific produce_value() method lookup
# @return any - The resolved value (static, from provider, or from object parameter)
def resolve(value, param_name)
if animation.is_value_provider(value)
return value.produce_value(param_name, self.engine.time_ms)
elif value != nil && isinstance(value, animation.parameterized_object)
# Handle parameterized objects (animations, etc.) by accessing their parameters
# Check that param_name is not nil to prevent runtime errors
if param_name == nil
raise "value_error", "Parameter name cannot be nil when resolving object parameter"
end
return value.get_param_value(param_name, self.engine.time_ms)
else
return value
end
end
# Produce a value by calling the stored closure
#
# @param name: string - Parameter name being requested
@ -69,112 +48,7 @@ class ClosureValueProvider : animation.value_provider
end
# Call the closure with the parameter self, name and time
return closure(self, name, time_ms)
end
# Mathematical helper methods for use in closures
# Minimum of two or more values
#
# @param a: number - First value
# @param b: number - Second value
# @param *args: number - Additional values (optional)
# @return number - Minimum value
def min(*args)
import math
return call(math.min, args)
end
# Maximum of two or more values
#
# @param a: number - First value
# @param b: number - Second value
# @param *args: number - Additional values (optional)
# @return number - Maximum value
def max(*args)
import math
return call(math.max, args)
end
# Absolute value
#
# @param x: number - Input value
# @return number - Absolute value
def abs(x)
import math
return math.abs(x)
end
# Round to nearest integer
#
# @param x: number - Input value
# @return int - Rounded value
def round(x)
import math
return int(math.round(x))
end
# Square root with integer handling
# For integers, treats 1.0 as 255 (full scale)
#
# @param x: number - Input value
# @return number - Square root
def sqrt(x)
import math
# If x is an integer in 0-255 range, scale to 0-1 for sqrt, then back
if type(x) == 'int' && x >= 0 && x <= 255
var normalized = x / 255.0
return int(math.sqrt(normalized) * 255)
else
return math.sqrt(x)
end
end
# Scale a value from one range to another using tasmota.scale_int
#
# @param v: number - Value to scale
# @param from_min: number - Source range minimum
# @param from_max: number - Source range maximum
# @param to_min: number - Target range minimum
# @param to_max: number - Target range maximum
# @return int - Scaled value
def scale(v, from_min, from_max, to_min, to_max)
return tasmota.scale_int(v, from_min, from_max, to_min, to_max)
end
# Sine function using tasmota.sine_int (works on integers)
# Input angle is in 0-255 range (mapped to 0-360 degrees)
# Output is in -255 to 255 range (mapped from -1.0 to 1.0)
#
# @param angle: number - Angle in 0-255 range (0-360 degrees)
# @return int - Sine value in -255 to 255 range
def sin(angle)
# Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)
var tasmota_angle = tasmota.scale_int(angle, 0, 255, 0, 32767)
# Get sine value from -4096 to 4096 (representing -1.0 to 1.0)
var sine_val = tasmota.sine_int(tasmota_angle)
# Map from -4096..4096 to -255..255 for integer output
return tasmota.scale_int(sine_val, -4096, 4096, -255, 255)
end
# Cosine function using tasmota.sine_int with phase shift
# Input angle is in 0-255 range (mapped to 0-360 degrees)
# Output is in -255 to 255 range (mapped from -1.0 to 1.0)
# Note: This matches the oscillator COSINE behavior (starts at minimum, not maximum)
#
# @param angle: number - Angle in 0-255 range (0-360 degrees)
# @return int - Cosine value in -255 to 255 range
def cos(angle)
# Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)
var tasmota_angle = tasmota.scale_int(angle, 0, 255, 0, 32767)
# Get cosine value by shifting sine by -90 degrees (matches oscillator behavior)
var cosine_val = tasmota.sine_int(tasmota_angle - 8192)
# Map from -4096..4096 to -255..255 for integer output
return tasmota.scale_int(cosine_val, -4096, 4096, -255, 255)
return closure(self.engine, name, time_ms)
end
# String representation for debugging
@ -198,5 +72,28 @@ def create_closure_value(engine, closure)
return provider
end
# Helper method to resolve a value that can be either static or from a value provider
# This is equivalent to 'resolve_param' but with a shorter name
# and available in animation module
#
# @param value: any - Static value, value provider instance, or parameterized object
# @param param_name: string - Parameter name for specific produce_value() method lookup
# @return any - The resolved value (static, from provider, or from object parameter)
def animation_resolve(value, param_name, time_ms)
if animation.is_value_provider(value)
return value.produce_value(param_name, time_ms)
elif value != nil && isinstance(value, animation.parameterized_object)
# Handle parameterized objects (animations, etc.) by accessing their parameters
# Check that param_name is not nil to prevent runtime errors
if param_name == nil
raise "value_error", "Parameter name cannot be nil when resolving object parameter"
end
return value.get_param_value(param_name)
else
return value
end
end
return {'closure_value': ClosureValueProvider,
'create_closure_value': create_closure_value}
'create_closure_value': create_closure_value,
'resolve': animation_resolve}

View File

@ -86,6 +86,7 @@ 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)
super(self).on_param_changed(name, value)
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()

View File

@ -136,11 +136,7 @@ class CompositeColorProvider : animation.color_provider
# String representation of the provider
def tostring()
try
return f"CompositeColorProvider(providers={size(self.providers)}, blend_mode={self.blend_mode})"
except ..
return "CompositeColorProvider(uninitialized)"
end
end
end

View File

@ -24,7 +24,6 @@ var BOUNCE = 9
#@ solidify:OscillatorValueProvider,weak
class OscillatorValueProvider : animation.value_provider
# Non-parameter instance variables only
var origin # origin time in ms for cycle calculation
var value # current calculated value
# Static array for better solidification (moved from inline array)
@ -47,19 +46,20 @@ class OscillatorValueProvider : animation.value_provider
super(self).init(engine) # Initialize parameter system
# Initialize non-parameter instance variables
self.origin = 0 # Will be set when `start` is called
self.value = 0 # Will be calculated on first produce_value call
end
# Start/restart the oscillator at a specific time
#
# @param time_ms: int - Time in milliseconds to set as origin (optional, uses engine time if nil)
# start() is typically not called at beginning of animations for value providers.
# The start_time is set at the first call to produce_value().
# This method is mainly aimed at restarting the value provider start_time
# via the `restart` keyword in DSL.
#
# @param time_ms: int - Time in milliseconds to set as start_time (optional, uses engine time if nil)
# @return self for method chaining
def start(time_ms)
if time_ms == nil
time_ms = self.engine.time_ms
end
self.origin = time_ms
super(self).start(time_ms)
return self
end
@ -77,12 +77,15 @@ class OscillatorValueProvider : animation.value_provider
var phase = self.phase
var duty_cycle = self.duty_cycle
# Ensure time_ms is valid and initialize start_time if needed
time_ms = self._fix_time_ms(time_ms)
if duration == nil || duration <= 0
return min_value
end
# Calculate elapsed time since origin
var past = time_ms - self.origin
# Calculate elapsed time since start_time
var past = time_ms - self.start_time
if past < 0
past = 0
end
@ -92,7 +95,7 @@ class OscillatorValueProvider : animation.value_provider
# Handle cycle wrapping
if past >= duration
var cycles = past / duration
self.origin += cycles * duration
self.start_time += cycles * duration
past = past % duration
end

View File

@ -15,7 +15,6 @@ class RichPaletteColorProvider : animation.color_provider
var slots # Number of slots in the palette
var current_color # Current interpolated color (calculated during update)
var light_state # light_state instance for proper color calculations
var cycle_start # Time when the animation cycle started
# Parameter definitions
static var PARAMS = {
@ -35,7 +34,6 @@ class RichPaletteColorProvider : animation.color_provider
# Initialize non-parameter instance variables
self.current_color = 0xFFFFFFFF
self.cycle_start = self.engine.time_ms # Initialize cycle start time
self.slots = 0
# Create light_state instance for proper color calculations (reuse from Animate_palette)
@ -48,6 +46,7 @@ class RichPaletteColorProvider : animation.color_provider
# @param name: string - Name of the parameter that changed
# @param value: any - New value of the parameter
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "range_min" || name == "range_max" || name == "cycle_period" || name == "palette"
if (self.slots_arr != nil) || (self.value_arr != nil)
# only if they were already computed
@ -58,14 +57,14 @@ class RichPaletteColorProvider : animation.color_provider
# Start/restart the animation cycle at a specific time
#
# @param time_ms: int - Time in milliseconds to set as cycle start (optional, uses engine time if nil)
# @param time_ms: int - Time in milliseconds to set as start_time (optional, uses engine time if nil)
# @return self for method chaining
def start(time_ms)
# Compute arrays if they were not yet initialized
if (self.slots_arr == nil) && (self.value_arr == nil)
self._recompute_palette()
end
self.cycle_start = (time_ms != nil) ? time_ms : self.engine.time_ms
super(self).start(time_ms)
return self
end
@ -178,6 +177,9 @@ class RichPaletteColorProvider : animation.color_provider
# @param time_ms: int - Current time in milliseconds
# @return int - Color in ARGB format (0xAARRGGBB)
def produce_value(name, time_ms)
# Ensure time_ms is valid and initialize start_time if needed
time_ms = self._fix_time_ms(time_ms)
if (self.slots_arr == nil) && (self.value_arr == nil)
self._recompute_palette()
end
@ -210,8 +212,8 @@ class RichPaletteColorProvider : animation.color_provider
return final_color
end
# Calculate position in cycle using cycle_start
var elapsed = time_ms - self.cycle_start
# Calculate position in cycle using start_time
var elapsed = time_ms - self.start_time
var past = elapsed % cycle_period
# Find slot (exact algorithm from Animate_palette)

View File

@ -34,11 +34,7 @@ class StaticColorProvider : animation.color_provider
# String representation of the provider
def tostring()
try
return f"StaticColorProvider(color=0x{self.color:08X})"
except ..
return "StaticColorProvider(color=unset)"
end
end
end

View File

@ -54,11 +54,7 @@ class StaticValueProvider : animation.value_provider
# String representation of the provider
def tostring()
try
return f"StaticValueProvider(value={self.value})"
except ..
return "StaticValueProvider(value=unset)"
end
end
end

View File

@ -19,20 +19,12 @@ class StripLengthProvider : animation.value_provider
# @param time_ms: int - Current time in milliseconds (ignored)
# @return int - The strip length in pixels
def produce_value(name, time_ms)
if self.engine == nil
return 0
end
return self.engine.width
return self.engine ? self.engine.width : 0
end
# String representation of the provider
def tostring()
try
var length = self.engine != nil ? self.engine.width : 0
return f"StripLengthProvider(length={length})"
except ..
return "StripLengthProvider(length=unknown)"
end
return f"StripLengthProvider(length={self.engine ? self.engine.width :: 'unknown'})"
end
end

View File

@ -32,6 +32,10 @@ class ValueProvider : animation.parameterized_object
# special value providers that return coordinated distinct
# values for different parameter names.
#
# For value providers, start is typically not called because instances
# can be embedded in closures. So value providers must consider the first
# call to `produce_value()` as a start of their internal time reference.
#
# @param name: string - Parameter name being requested
# @param time_ms: int - Current time in milliseconds
# @return any - Value appropriate for the parameter type

File diff suppressed because it is too large Load Diff

View File

@ -76,11 +76,11 @@ assert_test(!engine.remove_animation(anim2), "Should not remove non-existent ani
# Test 3: Engine Lifecycle
print("\n--- Test 3: Engine Lifecycle ---")
assert_test(engine.start(), "Should start engine")
assert_test(engine.run(), "Should start engine")
assert_equals(engine.is_active(), true, "Engine should be active after start")
# Test that starting again doesn't break anything
engine.start()
engine.run()
assert_equals(engine.is_active(), true, "Engine should remain active after second start")
assert_test(engine.stop(), "Should stop engine")
@ -94,7 +94,7 @@ test_anim.color = 0xFFFF0000
test_anim.priority = 10
test_anim.name = "test"
engine.add(test_anim)
engine.start()
engine.run()
var current_time = tasmota.millis()
@ -255,7 +255,7 @@ assert_test(temp_reused, "Temp buffer object should be reused for efficiency")
# Test 10c: Runtime detection during on_tick()
print("\n--- Test 10c: Runtime detection during on_tick() ---")
dynamic_engine.start()
dynamic_engine.run()
# Add a test animation
var runtime_anim = animation.solid(dynamic_engine)

View File

@ -43,7 +43,7 @@ base_anim.priority = 10
base_anim.name = "base_red"
opacity_engine.add(base_anim)
opacity_engine.start()
opacity_engine.run()
# Create frame buffer and test rendering
var opacity_frame = animation.frame_buffer(10)
@ -78,7 +78,7 @@ print("\n--- Test 11c: Animation opacity rendering ---")
opacity_engine.clear()
opacity_engine.add(masked_anim)
opacity_engine.start()
opacity_engine.run()
# Start both animations
masked_anim.start()

View File

@ -46,18 +46,20 @@ assert(default_anim.color == 0xFFFFFFFF, "Default color should be white")
# Test start method
engine.time_ms = 1000
anim.start()
anim.update()
assert(anim.is_running == true, "Animation should be running after start")
assert(anim.start_time == 1000, "Animation start time should be 1000")
assert(anim.current_time == 1000, "Animation current time should be 1000")
# Test restart functionality - start() acts as restart
engine.time_ms = 2000
anim.start()
assert(anim.is_running == true, "Animation should be running after start")
assert(anim.start_time == 2000, "Animation start time should be 2000")
var first_start_time = anim.start_time
# Start again - should restart with new time
engine.time_ms = 3000
anim.start()
anim.start(engine.time_ms)
assert(anim.is_running == true, "Animation should still be running after restart")
assert(anim.start_time == 3000, "Animation should have new start time after restart")
@ -71,6 +73,7 @@ non_loop_anim.name = "non_loop"
non_loop_anim.color = 0xFF0000
engine.time_ms = 2000
non_loop_anim.start(2000)
non_loop_anim.update(2000)
assert(non_loop_anim.is_running == true, "Animation should be running after start")
# Update within duration
@ -78,7 +81,6 @@ engine.time_ms = 2500
var result = non_loop_anim.update(engine.time_ms)
assert(result == true, "Update should return true when animation is still running")
assert(non_loop_anim.is_running == true, "Animation should still be running")
assert(non_loop_anim.current_time == 2500, "Current time should be updated")
# Update after duration
engine.time_ms = 3100
@ -95,7 +97,8 @@ loop_anim.opacity = 255
loop_anim.name = "loop"
loop_anim.color = 0xFF0000
engine.time_ms = 4000
loop_anim.start(4000)
loop_anim.start(engine.time_ms)
loop_anim.update(engine.time_ms) # update must be explictly called to start time
# Update after one loop
engine.time_ms = 5100

View File

@ -54,7 +54,7 @@ def test_atomic_closure_batch_execution()
# Start sequence
tasmota.set_millis(10000)
engine.start()
engine.run()
engine.on_tick(10000)
seq_manager.start(10000)
@ -123,7 +123,7 @@ def test_multiple_consecutive_closures()
# Start sequence
tasmota.set_millis(20000)
engine.start()
engine.run()
engine.on_tick(20000)
seq_manager.start(20000)
@ -177,7 +177,7 @@ def test_closure_batch_at_sequence_start()
# Start sequence
tasmota.set_millis(30000)
engine.start()
engine.run()
engine.on_tick(30000)
seq_manager.start(30000)
@ -217,7 +217,7 @@ def test_repeat_sequence_closure_batching()
# Start sequence
tasmota.set_millis(40000)
engine.start()
engine.run()
engine.on_tick(40000)
seq_manager.start(40000)
@ -294,7 +294,7 @@ def test_black_frame_fix_integration()
# Start sequence
tasmota.set_millis(50000)
engine.start()
engine.run()
engine.on_tick(50000)
seq_manager.start(50000)

View File

@ -68,6 +68,7 @@ print(f"Updated blue breathe curve_factor: {blue_breathe.curve_factor}")
engine.time_ms = 1000 # Set engine time for testing
var start_time = engine.time_ms
blue_breathe.start(start_time)
blue_breathe.produce_value(nil, start_time) # force first tick
print(f"Started blue breathe color provider at time: {start_time}")
# Cache duration for performance (following specification)
@ -107,20 +108,27 @@ var curve_1_provider = animation.breathe_color(engine)
curve_1_provider.base_color = 0xFF00FF00 # Green
curve_1_provider.curve_factor = 1
curve_1_provider.duration = 2000
curve_1_provider.min_brightness = 50 # Set non-zero minimum to see differences
curve_1_provider.max_brightness = 255
curve_1_provider.start(engine.time_ms)
curve_1_provider.produce_value(nil, start_time) # force first tick
var curve_5_provider = animation.breathe_color(engine)
curve_5_provider.base_color = 0xFF00FF00 # Green
curve_5_provider.curve_factor = 5
curve_5_provider.duration = 2000
curve_5_provider.min_brightness = 50 # Set non-zero minimum to see differences
curve_5_provider.max_brightness = 255
curve_5_provider.start(engine.time_ms)
curve_5_provider.produce_value(nil, start_time) # force first tick
# Compare curve effects at quarter cycle
engine.time_ms = engine.time_ms + 500 # 1/4 of 2000ms cycle
# Compare curve effects at different cycle points where differences will be visible
# Test at 3/8 cycle (375ms) where cosine is around 0.7, giving more room for curve differences
engine.time_ms = engine.time_ms + 375 # 3/8 of 2000ms cycle
var curve_1_color = curve_1_provider.produce_value("color", engine.time_ms)
var curve_5_color = curve_5_provider.produce_value("color", engine.time_ms)
print(f"Curve factor 1 at 1/4 cycle: 0x{curve_1_color :08x}")
print(f"Curve factor 5 at 1/4 cycle: 0x{curve_5_color :08x}")
print(f"Curve factor 1 at 3/8 cycle: 0x{curve_1_color :08x}")
print(f"Curve factor 5 at 3/8 cycle: 0x{curve_5_color :08x}")
# Test pulsating color provider factory
var pulsating = animation.pulsating_color(engine)
@ -163,15 +171,16 @@ brightness_test.min_brightness = 50
brightness_test.max_brightness = 200
brightness_test.duration = 1000
brightness_test.start(engine.time_ms)
brightness_test.produce_value(nil, start_time) # force first tick
# At minimum (start of cosine cycle)
engine.time_ms = engine.time_ms
# Test at quarter cycle (should be near minimum for this cosine implementation)
engine.time_ms = engine.time_ms + 250 # Quarter cycle
var min_color = brightness_test.produce_value("color", engine.time_ms)
var min_brightness_actual = min_color & 0xFF # Blue component should match brightness
print(f"Min brightness test - expected around 50, got: {min_brightness_actual}")
print(f"Min brightness test - expected around 53, got: {min_brightness_actual}")
# At maximum (middle of cosine cycle)
engine.time_ms = engine.time_ms + 500 # Half cycle
# Test at three-quarter cycle (should be near maximum for this cosine implementation)
engine.time_ms = engine.time_ms + 500 # Move to 3/4 cycle
var max_color = brightness_test.produce_value("color", engine.time_ms)
var max_brightness_actual = max_color & 0xFF # Blue component should match brightness
print(f"Max brightness test - expected around 200, got: {max_brightness_actual}")
@ -180,6 +189,7 @@ print(f"Max brightness test - expected around 200, got: {max_brightness_actual}"
var alpha_test = animation.breathe_color(engine)
alpha_test.base_color = 0x80FF0000 # Red with 50% alpha
alpha_test.start(engine.time_ms)
alpha_test.produce_value(nil, start_time) # force first tick
var alpha_color = alpha_test.produce_value("color", engine.time_ms)
var alpha_actual = (alpha_color >> 24) & 0xFF
print(f"Alpha preservation test - expected 128, got: {alpha_actual}")
@ -212,9 +222,9 @@ assert(color_1_4 != color_3_4, "Colors should be different at quarter vs three-q
# Test that curve factors produce different results
assert(curve_1_color != curve_5_color, "Different curve factors should produce different colors")
# Test brightness range is respected
assert(min_brightness_actual >= 40 && min_brightness_actual <= 60, "Min brightness should be around 50")
assert(max_brightness_actual >= 180 && max_brightness_actual <= 220, "Max brightness should be around 200")
# Test brightness range is respected (allowing for curve factor and timing variations)
assert(min_brightness_actual >= 45 && min_brightness_actual <= 65, "Min brightness should be around 53")
assert(max_brightness_actual >= 150 && max_brightness_actual <= 170, "Max brightness should be around 160")
print("All tests completed successfully!")
return true

View File

@ -27,7 +27,6 @@ def test_closure_value_provider()
# Test 2: Set a simple closure
var f = def(self, name, time_ms) return time_ms / 100 end
print(f">> {f=} {provider=}")
provider.closure = f
result = provider.produce_value("brightness", 1000)
assert(result == 10, f"Expected 10, got {result}")
@ -73,36 +72,35 @@ def test_closure_value_provider()
static_provider.value = 100
provider.closure = def(self, name, time_ms)
# Use self.resolve to get value from another provider
var base_value = self.resolve(static_provider, name, time_ms)
# Use animation.resolve to get value from another provider
var base_value = animation.resolve(static_provider, name, time_ms)
return base_value * 2
end
result = provider.produce_value("test", 2000)
# static_provider returns 100, then multiply by 2 = 200
assert(result == 200, f"Expected 200, got {result}")
print("✓ self.resolve helper method works with value providers")
print("✓ animation.resolve helper method works with value providers")
# Test 6: Test self.resolve with static value and value provider
# Test 6: Test animation.resolve with static value and value provider
provider.closure = def(self, name, time_ms)
var static_value = self.resolve(50, name, time_ms) # Static value
var dynamic_value = self.resolve(static_provider, name, time_ms) # Value provider
var static_value = animation.resolve(50, name, time_ms) # Static value
var dynamic_value = animation.resolve(static_provider, name, time_ms) # Value provider
return static_value + dynamic_value
end
result = provider.produce_value("test", 1000)
# static: 50, dynamic: 100, total: 150
assert(result == 150, f"Expected 150, got {result}")
print("✓ self.resolve works with both static values and value providers")
print("✓ animation.resolve works with both static values and value providers")
# Test 7: Test the use case from documentation - arithmetic with another provider
var oscillator = animation.oscillator_value(engine)
oscillator.min_value = 10
oscillator.max_value = 20
oscillator.duration = 1000
provider.closure = def(self, name, time_ms)
var osc_value = self.resolve(oscillator, name, time_ms)
provider.closure = def(engine, name, time_ms)
var osc_value = animation.resolve(oscillator, name, time_ms)
return osc_value + 5 # Add 5 to oscillator value
end
@ -142,9 +140,9 @@ def test_closure_value_provider()
param3.value = 2
provider.closure = def(self, name, time_ms)
var p1 = self.resolve(param1, name, time_ms)
var p2 = self.resolve(param2, name, time_ms)
var p3 = self.resolve(param3, name, time_ms)
var p1 = animation.resolve(param1, name, time_ms)
var p2 = animation.resolve(param2, name, time_ms)
var p3 = animation.resolve(param3, name, time_ms)
if name == "arithmetic_complex"
return (p1 + p2) * p3 - 5 # (10 + 3) * 2 - 5 = 26 - 5 = 21
@ -173,9 +171,9 @@ def test_closure_value_provider()
# Test 10: Time-based expressions with multiple variables
provider.closure = def(self, name, time_ms)
var base_freq = self.resolve(param1, name, time_ms) # 10
var amplitude = self.resolve(param2, name, time_ms) # 3
var offset = self.resolve(param3, name, time_ms) # 2
var base_freq = animation.resolve(param1, name, time_ms) # 10
var amplitude = animation.resolve(param2, name, time_ms) # 3
var offset = animation.resolve(param3, name, time_ms) # 2
if name == "sine_wave_simulation"
# Simulate: amplitude * sin(time * base_freq / 1000) + offset
@ -259,14 +257,15 @@ def test_closure_math_methods()
# Test 1: min/max functions
provider.closure = def(self, name, time_ms)
print(f">> {name=} {animation._math=}")
if name == "min_test"
return self.min(5, 3, 8, 1, 9) # Should return 1
return animation._math.min(5, 3, 8, 1, 9) # Should return 1
elif name == "max_test"
return self.max(5, 3, 8, 1, 9) # Should return 9
return animation._math.max(5, 3, 8, 1, 9) # Should return 9
elif name == "min_two"
return self.min(10, 7) # Should return 7
return animation._math.min(10, 7) # Should return 7
elif name == "max_two"
return self.max(10, 7) # Should return 10
return animation._math.max(10, 7) # Should return 10
else
return 0
end
@ -286,13 +285,13 @@ def test_closure_math_methods()
# Test 2: abs function
provider.closure = def(self, name, time_ms)
if name == "abs_positive"
return self.abs(42) # Should return 42
return animation._math.abs(42) # Should return 42
elif name == "abs_negative"
return self.abs(-17) # Should return 17
return animation._math.abs(-17) # Should return 17
elif name == "abs_zero"
return self.abs(0) # Should return 0
return animation._math.abs(0) # Should return 0
elif name == "abs_float"
return self.abs(-3.14) # Should return 3.14
return animation._math.abs(-3.14) # Should return 3.14
else
return 0
end
@ -312,13 +311,13 @@ def test_closure_math_methods()
# Test 3: round function
provider.closure = def(self, name, time_ms)
if name == "round_up"
return self.round(3.7) # Should return 4
return animation._math.round(3.7) # Should return 4
elif name == "round_down"
return self.round(3.2) # Should return 3
return animation._math.round(3.2) # Should return 3
elif name == "round_half"
return self.round(3.5) # Should return 4
return animation._math.round(3.5) # Should return 4
elif name == "round_negative"
return self.round(-2.8) # Should return -3
return animation._math.round(-2.8) # Should return -3
else
return 0
end
@ -338,13 +337,13 @@ def test_closure_math_methods()
# Test 4: sqrt function with integer handling
provider.closure = def(self, name, time_ms)
if name == "sqrt_integer_255"
return self.sqrt(255) # Should return 255 (full scale)
return animation._math.sqrt(255) # Should return 255 (full scale)
elif name == "sqrt_integer_64"
return self.sqrt(64) # Should return ~127 (sqrt(64/255)*255)
return animation._math.sqrt(64) # Should return ~127 (sqrt(64/255)*255)
elif name == "sqrt_integer_0"
return self.sqrt(0) # Should return 0
return animation._math.sqrt(0) # Should return 0
elif name == "sqrt_float"
return self.sqrt(16.0) # Should return 4.0
return animation._math.sqrt(16.0) # Should return 4.0
else
return 0
end
@ -364,11 +363,11 @@ def test_closure_math_methods()
# Test 5: scale function
provider.closure = def(self, name, time_ms)
if name == "scale_basic"
return self.scale(50, 0, 100, 0, 255) # Should return ~127
return animation._math.scale(50, 0, 100, 0, 255) # Should return ~127
elif name == "scale_reverse"
return self.scale(25, 0, 100, 255, 0) # Should return ~191
return animation._math.scale(25, 0, 100, 255, 0) # Should return ~191
elif name == "scale_negative"
return self.scale(0, -50, 50, -100, 100) # Should return 0
return animation._math.scale(0, -50, 50, -100, 100) # Should return 0
else
return 0
end
@ -386,13 +385,13 @@ def test_closure_math_methods()
# Test 6: sin function
provider.closure = def(self, name, time_ms)
if name == "sin_0"
return self.sin(0) # sin(0°) = 0
return animation._math.sin(0) # sin(0°) = 0
elif name == "sin_64"
return self.sin(64) # sin(90°) = 1 -> 255
return animation._math.sin(64) # sin(90°) = 1 -> 255
elif name == "sin_128"
return self.sin(128) # sin(180°) = 0
return animation._math.sin(128) # sin(180°) = 0
elif name == "sin_192"
return self.sin(192) # sin(270°) = -1 -> -255
return animation._math.sin(192) # sin(270°) = -1 -> -255
else
return 0
end
@ -412,13 +411,13 @@ def test_closure_math_methods()
# Test 7: cos function (matches oscillator COSINE behavior)
provider.closure = def(self, name, time_ms)
if name == "cos_0"
return self.cos(0) # Oscillator cosine at 0° = minimum -> -255
return animation._math.cos(0) # Oscillator cosine at 0° = minimum -> -255
elif name == "cos_64"
return self.cos(64) # Oscillator cosine at 90° = ~0
return animation._math.cos(64) # Oscillator cosine at 90° = ~0
elif name == "cos_128"
return self.cos(128) # Oscillator cosine at 180° = maximum -> 255
return animation._math.cos(128) # Oscillator cosine at 180° = maximum -> 255
elif name == "cos_192"
return self.cos(192) # Oscillator cosine at 270° = ~0
return animation._math.cos(192) # Oscillator cosine at 270° = ~0
else
return 0
end
@ -439,9 +438,9 @@ def test_closure_math_methods()
provider.closure = def(self, name, time_ms)
if name == "complex_math"
var angle = time_ms % 256 # 0-255 angle based on time
var sine_val = self.abs(self.sin(angle)) # Absolute sine value
var scaled = self.scale(sine_val, 0, 255, 50, 200) # Scale to 50-200 range
return self.min(self.max(scaled, 75), 175) # Clamp to 75-175 range
var sine_val = animation._math.abs(animation._math.sin(angle)) # Absolute sine value
var scaled = animation._math.scale(sine_val, 0, 255, 50, 200) # Scale to 50-200 range
return animation._math.min(animation._math.max(scaled, 75), 175) # Clamp to 75-175 range
else
return 0
end

View File

@ -152,6 +152,7 @@ pos_comet.speed = 2560 # 10 pixels/sec (10 * 256)
engine.time_ms = 1000
var start_time = engine.time_ms
pos_comet.start(start_time)
pos_comet.update(start_time)
engine.time_ms = start_time + 1000 # 1 second later
pos_comet.update(engine.time_ms)
@ -199,6 +200,7 @@ wrap_comet.wrap_around = 1 # Enable wrapping
small_engine.time_ms = 3000
start_time = small_engine.time_ms
wrap_comet.start(start_time)
wrap_comet.update(start_time)
small_engine.time_ms = start_time + 2000 # 2 seconds - should wrap multiple times
wrap_comet.update(small_engine.time_ms)
var strip_length_subpixels = 10 * 256
@ -215,6 +217,7 @@ bounce_comet.wrap_around = 0 # Disable wrapping (enable bouncing)
small_engine.time_ms = 4000
start_time = small_engine.time_ms
bounce_comet.start(start_time)
bounce_comet.update(small_engine.time_ms)
small_engine.time_ms = start_time + 200 # Should hit the end and bounce
bounce_comet.update(small_engine.time_ms)
# Direction should have changed due to bouncing
@ -232,8 +235,6 @@ render_comet.speed = 256 # Slow (1 pixel/sec)
small_engine.time_ms = 5000
render_comet.start(small_engine.time_ms)
# Update once to initialize position
render_comet.update(small_engine.time_ms)
# Clear frame and render
@ -295,6 +296,7 @@ assert_equals(strip_length, 30, "Strip length should come from engine")
# Test engine time usage
engine.time_ms = 7000
engine_comet.start(engine.time_ms)
engine_comet.update(engine.time_ms)
assert_equals(engine_comet.start_time, 7000, "Animation should use engine time for start")
# Test Results

View File

@ -204,7 +204,7 @@ def test_sequence_processing()
assert(string.find(berry_code, "red_anim") >= 0, "Should reference animation")
assert(string.find(berry_code, ".push_play_step(red_anim_, 2000)") >= 0, "Should create play step")
assert(string.find(berry_code, "engine.add(demo_)") >= 0, "Should add sequence manager")
assert(string.find(berry_code, "engine.start()") >= 0, "Should start engine")
assert(string.find(berry_code, "engine.run()") >= 0, "Should start engine")
# Test repeat in sequence
var repeat_seq_dsl = "color custom_blue = 0x0000FF\n" +

View File

@ -159,7 +159,7 @@ def test_sequences()
assert(string.find(berry_code, "var test_seq_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
assert(string.find(berry_code, ".push_play_step(") >= 0, "Should add play step")
assert(string.find(berry_code, "3000)") >= 0, "Should reference duration")
assert(string.find(berry_code, "engine.start()") >= 0, "Should start engine")
assert(string.find(berry_code, "engine.run()") >= 0, "Should start engine")
print("✓ Sequences test passed")
return true
@ -364,29 +364,29 @@ def test_multiple_run_statements()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile multiple run statements")
# Count engine.start() calls - should be exactly 1
# Count engine.run() calls - should be exactly 1
var lines = string.split(berry_code, "\n")
var start_count = 0
for line : lines
if string.find(line, "engine.start()") >= 0
if string.find(line, "engine.run()") >= 0
start_count += 1
end
end
assert(start_count == 1, f"Should have exactly 1 engine.start() call, found {start_count}")
assert(start_count == 1, f"Should have exactly 1 engine.run() call, found {start_count}")
# Check that all animations are added to the 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
# Verify the engine.run() comes after all animations are added
var start_line_index = -1
var last_add_line_index = -1
for i : 0..size(lines)-1
var line = lines[i]
if string.find(line, "engine.start()") >= 0
if string.find(line, "engine.run()") >= 0
start_line_index = i
end
if string.find(line, "engine.add(") >= 0
@ -394,7 +394,7 @@ def test_multiple_run_statements()
end
end
assert(start_line_index > last_add_line_index, "engine.start() should come after all engine.add_* calls")
assert(start_line_index > last_add_line_index, "engine.run() should come after all engine.add_* calls")
# Test with mixed animations and sequences
var mixed_dsl = "# strip length 30 # TEMPORARILY DISABLED\n" +
@ -414,16 +414,16 @@ def test_multiple_run_statements()
var mixed_berry_code = animation_dsl.compile(mixed_dsl)
assert(mixed_berry_code != nil, "Should compile mixed run statements")
# Count engine.start() calls in mixed scenario
# Count engine.run() calls in mixed scenario
var mixed_lines = string.split(mixed_berry_code, "\n")
var mixed_start_count = 0
for line : mixed_lines
if string.find(line, "engine.start()") >= 0
if string.find(line, "engine.run()") >= 0
mixed_start_count += 1
end
end
assert(mixed_start_count == 1, f"Mixed scenario should have exactly 1 engine.start() call, found {mixed_start_count}")
assert(mixed_start_count == 1, f"Mixed scenario should have exactly 1 engine.run() call, found {mixed_start_count}")
# Check that both animation and sequence are handled
assert(string.find(mixed_berry_code, "engine.add(red_anim_)") >= 0, "Should add animation to engine")
@ -481,14 +481,14 @@ def test_computed_values()
assert(computed_code != nil, "Should compile computed values")
# Check for single resolve calls (no double wrapping)
var expected_single_resolve = "self.abs(self.resolve(strip_len_) / 4)"
var expected_single_resolve = "animation._math.abs(animation.resolve(strip_len_) / 4)"
assert(string.find(computed_code, expected_single_resolve) >= 0, "Should generate single resolve call in computed expression")
# Check that there are no double resolve calls
var double_resolve_count = 0
var pos = 0
while true
pos = string.find(computed_code, "self.resolve(self.resolve(", pos)
pos = string.find(computed_code, "animation.resolve(self.resolve(", pos)
if pos < 0
break
end
@ -545,13 +545,13 @@ def test_computed_values()
assert(nested_closure_count == 0, f"Should have no nested closures, found {nested_closure_count}")
# Verify specific complex expression patterns
var expected_complex_tail = "self.resolve(strip_len_) / 8 + (2 * self.resolve(strip_len_)) - 10"
var expected_complex_tail = "animation.resolve(strip_len_) / 8 + (2 * animation.resolve(strip_len_)) - 10"
assert(string.find(complex_code, expected_complex_tail) >= 0, "Should generate correct complex tail_length expression")
var expected_complex_speed = "(self.resolve(base_value_) + self.resolve(strip_len_)) * 2.5"
var expected_complex_speed = "(animation.resolve(base_value_) + animation.resolve(strip_len_)) * 2.5"
assert(string.find(complex_code, expected_complex_speed) >= 0, "Should generate correct complex speed expression")
var expected_complex_priority = "self.max(1, self.min(10, self.resolve(strip_len_) / 6))"
var expected_complex_priority = "animation._math.max(1, animation._math.min(10, animation.resolve(strip_len_) / 6))"
assert(string.find(complex_code, expected_complex_priority) >= 0, "Should generate correct complex priority expression with math functions")
# Test simple expressions that don't need closures
@ -582,9 +582,9 @@ def test_computed_values()
assert(math_code != nil, "Should compile mathematical expressions")
# Check that mathematical functions are prefixed with self. in closures
assert(string.find(math_code, "self.max(1, self.min(") >= 0, "Should prefix math functions with self. in closures")
assert(string.find(math_code, "self.abs(") >= 0, "Should prefix abs function with self. in closures")
assert(string.find(math_code, "self.round(") >= 0, "Should prefix round function with self. in closures")
assert(string.find(math_code, "animation._math.max(1, animation._math.min(") >= 0, "Should prefix math functions with animation._math. in closures")
assert(string.find(math_code, "animation._math.abs(") >= 0, "Should prefix abs function with self. in closures")
assert(string.find(math_code, "animation._math.round(") >= 0, "Should prefix round function with self. in closures")
print("✓ Computed values test passed")
return true

Some files were not shown because too many files have changed in this diff Show More