Berry animation remove noise_animation (#24291)

This commit is contained in:
s-hadinger 2026-01-02 16:46:15 +01:00 committed by GitHub
parent f5d8ec43fc
commit 36424dd8e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1577 additions and 2890 deletions

View File

@ -167,7 +167,6 @@ Animation|Description
`palette_gradient_animation`|Gradient patterns with palette colors `palette_gradient_animation`|Gradient patterns with palette colors
`palette_meter_animation`|Meter/bar patterns `palette_meter_animation`|Meter/bar patterns
`gradient_meter_animation`|VU meter with gradient and peak hold `gradient_meter_animation`|VU meter with gradient and peak hold
`noise_animation`|Perlin noise patterns
`wave_animation`|Wave motion effects `wave_animation`|Wave motion effects
## Documentation ## Documentation

View File

@ -30,7 +30,6 @@ ParameterizedObject (base class with parameter management and playable interface
│ ├── CometAnimation (moving comet with tail) │ ├── CometAnimation (moving comet with tail)
│ ├── FireAnimation (realistic fire effect) │ ├── FireAnimation (realistic fire effect)
│ ├── TwinkleAnimation (twinkling stars effect) │ ├── TwinkleAnimation (twinkling stars effect)
│ ├── NoiseAnimation (Perlin noise patterns)
│ ├── WaveAnimation (wave motion effects) │ ├── WaveAnimation (wave motion effects)
│ └── RichPaletteAnimation (smooth palette transitions) │ └── RichPaletteAnimation (smooth palette transitions)
├── SequenceManager (orchestrates animation sequences) ├── SequenceManager (orchestrates animation sequences)
@ -654,76 +653,6 @@ audio_meter.level = audio_level
**Factory**: `animation.gradient_meter_animation(engine)` **Factory**: `animation.gradient_meter_animation(engine)`
### NoiseAnimation
Creates pseudo-random noise patterns with configurable scale, speed, and fractal complexity. Perfect for organic, natural-looking effects like clouds, fire textures, or abstract patterns. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color` | instance | nil | - | Color provider for noise mapping (nil = rainbow) |
| `scale` | int | 50 | 1-255 | Noise scale/frequency (lower = larger patterns) |
| `speed` | int | 30 | 0-255 | Animation speed (0 = static pattern) |
| `octaves` | int | 1 | 1-4 | Number of noise octaves for fractal complexity |
| `persistence` | int | 128 | 0-255 | How much each octave contributes to final pattern |
| `seed` | int | 12345 | 0-65535 | Random seed for reproducible patterns |
| *(inherits all Animation parameters)* | | | | |
#### Noise Characteristics
**Scale Effects:**
- **Low scale (10-30)**: Large, flowing patterns
- **Medium scale (40-80)**: Balanced detail and flow
- **High scale (100-200)**: Fine, detailed textures
**Octave Effects:**
- **1 octave**: Smooth, simple patterns
- **2 octaves**: Added medium-frequency detail
- **3+ octaves**: Complex, natural-looking textures
**Speed Effects:**
- **Static (0)**: Fixed pattern for backgrounds
- **Slow (10-40)**: Gentle, organic movement
- **Fast (80-200)**: Dynamic, energetic patterns
#### Usage Examples
```berry
# Rainbow noise with medium detail
animation rainbow_noise = noise_animation(
scale=60,
speed=40,
octaves=1
)
# Blue fire texture with fractal detail
color blue_fire = 0xFF0066FF
animation blue_texture = noise_animation(
color=blue_fire,
scale=120,
speed=60,
octaves=3,
persistence=100
)
# Static cloud pattern
animation cloud_pattern = noise_animation(
color=white,
scale=30,
speed=0,
octaves=2
)
```
#### Common Use Cases
- **Ambient Lighting**: Slow, low-scale noise for background ambiance
- **Fire Effects**: Orange/red colors with medium scale and speed
- **Water Effects**: Blue/cyan colors with flowing movement
- **Cloud Simulation**: White/gray colors with large-scale patterns
- **Abstract Art**: Rainbow colors with high detail and multiple octaves
### PulseAnimation ### PulseAnimation
Creates a pulsing effect oscillating between min and max brightness. Inherits from `Animation`. Creates a pulsing effect oscillating between min and max brightness. Inherits from `Animation`.
@ -1230,7 +1159,6 @@ run gradient_wave
- Each animation uses approximately 4 bytes per pixel for color storage - Each animation uses approximately 4 bytes per pixel for color storage
- Fire animation includes additional flicker calculations - Fire animation includes additional flicker calculations
- Gradient animation requires color interpolation calculations - Gradient animation requires color interpolation calculations
- Noise animation includes pseudo-random pattern generation
- Consider strip length impact on transformation calculations - Consider strip length impact on transformation calculations
## Parameter Constraints ## Parameter Constraints

View File

@ -1441,7 +1441,6 @@ Animation classes create visual effects on LED strips:
| `fire_animation` | Realistic fire simulation | | `fire_animation` | Realistic fire simulation |
| `twinkle_animation` | Twinkling stars effect | | `twinkle_animation` | Twinkling stars effect |
| `gradient_animation` | Color gradient effects | | `gradient_animation` | Color gradient effects |
| `noise_animation` | Perlin noise-based patterns |
| `wave_animation` | Wave propagation effects | | `wave_animation` | Wave propagation effects |
| `rich_palette_animation` | Palette-based color cycling | | `rich_palette_animation` | Palette-based color cycling |
| `palette_wave_animation` | Wave patterns using palettes | | `palette_wave_animation` | Wave patterns using palettes |

View File

@ -145,8 +145,6 @@ import "animations/gradient" as gradient_animation
register_to_animation(gradient_animation) register_to_animation(gradient_animation)
import "animations/palette_meter" as palette_meter_animation import "animations/palette_meter" as palette_meter_animation
register_to_animation(palette_meter_animation) register_to_animation(palette_meter_animation)
import "animations/noise" as noise_animation
register_to_animation(noise_animation)
# import "animations/plasma" as plasma_animation # import "animations/plasma" as plasma_animation
# register_to_animation(plasma_animation) # register_to_animation(plasma_animation)
# import "animations/sparkle" as sparkle_animation # import "animations/sparkle" as sparkle_animation

View File

@ -1,288 +0,0 @@
# Noise animation effect for Berry Animation Framework
#
# This animation creates pseudo-random noise patterns with configurable
# scale, speed, and color mapping through palettes or single colors.
import "./core/param_encoder" as encode_constraints
#@ solidify:NoiseAnimation,weak
class NoiseAnimation : animation.animation
# Non-parameter instance variables only
var current_colors # Array of current colors for each pixel
var time_offset # Current time offset for animation
var noise_table # Pre-computed noise values for performance
# Parameter definitions following new specification
static var PARAMS = animation.enc_params({
"color": {"default": nil},
"scale": {"min": 1, "max": 255, "default": 50},
"speed": {"min": 0, "max": 255, "default": 30},
"octaves": {"min": 1, "max": 4, "default": 1},
"persistence": {"min": 0, "max": 255, "default": 128},
"seed": {"min": 0, "max": 65535, "default": 12345}
})
# Initialize a new Noise animation
def init(engine)
# Call parent constructor with engine only
super(self).init(engine)
# Initialize non-parameter instance variables only
var strip_length = self.engine.strip_length
self.current_colors = []
self.current_colors.resize(strip_length)
self.time_offset = 0
# Initialize colors to black
var i = 0
while i < strip_length
self.current_colors[i] = 0xFF000000
i += 1
end
# Initialize noise table - will be done in start method
self.noise_table = []
# Set default color if not set
if self.color == nil
var rainbow_provider = animation.rich_palette(engine)
rainbow_provider.colors = animation.PALETTE_RAINBOW
rainbow_provider.period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
self.color = rainbow_provider
end
end
# Override start method for initialization
def start(time_ms)
# Call parent start first
super(self).start(time_ms)
# Initialize noise table with current seed
self._init_noise_table()
# Reset time offset
self.time_offset = 0
return self
end
# Initialize noise lookup table for performance
def _init_noise_table()
self.noise_table = []
self.noise_table.resize(256)
# Generate pseudo-random values using seed
var current_seed = self.seed
var rng_state = current_seed
var i = 0
while i < 256
rng_state = (rng_state * 1103515245 + 12345) & 0x7FFFFFFF
self.noise_table[i] = rng_state % 256
i += 1
end
end
# Override setmember to handle color conversion
def setmember(name, value)
if name == "color" && type(value) == "int"
# Convert integer color to gradient palette from black to color
var palette = bytes()
palette.add(0x00, 1) # Position 0: black
palette.add(0x00, 1) # R
palette.add(0x00, 1) # G
palette.add(0x00, 1) # B
palette.add(0xFF, 1) # Position 255: full color
palette.add((value >> 16) & 0xFF, 1) # R
palette.add((value >> 8) & 0xFF, 1) # G
palette.add(value & 0xFF, 1) # B
var gradient_provider = animation.rich_palette(self.engine)
gradient_provider.colors = palette
gradient_provider.period = 5000
gradient_provider.transition_type = 1
gradient_provider.brightness = 255
# Set the gradient provider instead of the integer
super(self).setmember(name, gradient_provider)
else
# Use parent implementation for other parameters
super(self).setmember(name, value)
end
end
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "seed"
self._init_noise_table()
end
# Update current_colors array size when strip length changes via engine
var new_strip_length = self.engine.strip_length
if size(self.current_colors) != new_strip_length
self.current_colors.resize(new_strip_length)
var i = size(self.current_colors)
while i < new_strip_length
self.current_colors[i] = 0xFF000000
i += 1
end
end
end
# Simple noise function using lookup table
def _noise_1d(x)
var ix = int(x) & 255
var fx = x - int(x)
# Get noise values at integer positions
var a = self.noise_table[ix]
var b = self.noise_table[(ix + 1) & 255]
# Linear interpolation using integer math
var lerp_amount = tasmota.scale_uint(int(fx * 256), 0, 256, 0, 255)
return tasmota.scale_uint(lerp_amount, 0, 255, a, b)
end
# Fractal noise with multiple octaves
def _fractal_noise(x, time_offset)
var value = 0
var amplitude = 255
var current_scale = self.scale
var current_octaves = self.octaves
var current_persistence = self.persistence
var frequency = current_scale
var max_value = 0
var octave = 0
while octave < current_octaves
var sample_x = tasmota.scale_uint(x * frequency, 0, 255 * 255, 0, 255) + time_offset
var noise_val = self._noise_1d(sample_x)
value += tasmota.scale_uint(noise_val, 0, 255, 0, amplitude)
max_value += amplitude
amplitude = tasmota.scale_uint(amplitude, 0, 255, 0, current_persistence)
frequency = frequency * 2
if frequency > 255
frequency = 255
end
octave += 1
end
# Normalize to 0-255 range
if max_value > 0
value = tasmota.scale_uint(value, 0, max_value, 0, 255)
end
return value
end
# Update animation state
def update(time_ms)
super(self).update(time_ms)
# Update time offset based on speed
var current_speed = self.speed
if current_speed > 0
var elapsed = time_ms - self.start_time
# Speed: 0-255 maps to 0-5 units per second
var units_per_second = tasmota.scale_uint(current_speed, 0, 255, 0, 5)
if units_per_second > 0
self.time_offset = (elapsed * units_per_second / 1000) % 256
end
end
# Calculate noise colors
self._calculate_noise(time_ms)
end
# Calculate noise colors for all pixels
def _calculate_noise(time_ms)
var strip_length = self.engine.strip_length
var current_color = self.color
var i = 0
while i < strip_length
# Calculate noise value for this pixel
var noise_value = self._fractal_noise(i, self.time_offset)
# Get color from provider
var color = 0xFF000000
# If the color is a provider that supports get_color_for_value, use it
if animation.is_color_provider(current_color) && current_color.get_color_for_value != nil
color = current_color.get_color_for_value(noise_value, 0)
else
# Use resolve_value with noise influence
color = self.resolve_value(current_color, "color", time_ms + noise_value * 10)
end
self.current_colors[i] = color
i += 1
end
end
# Render noise to frame buffer
def render(frame, time_ms, strip_length)
var i = 0
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
end
i += 1
end
return true
end
end
# Factory functions following new specification
# Create a rainbow noise animation preset
def noise_rainbow(engine)
var anim = animation.noise_animation(engine)
# Set up rainbow color provider
var rainbow_provider = animation.rich_palette(engine)
rainbow_provider.colors = animation.PALETTE_RAINBOW
rainbow_provider.period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
anim.color = rainbow_provider
anim.scale = 50
anim.speed = 30
anim.octaves = 1
return anim
end
# Create a single color noise animation preset
def noise_single_color(engine)
var anim = animation.noise_animation(engine)
# Set up a simple white color - user can change it after creation
anim.color = 0xFFFFFFFF
anim.scale = 50
anim.speed = 30
anim.octaves = 1
return anim
end
# Create a fractal noise animation preset
def noise_fractal(engine)
var anim = animation.noise_animation(engine)
# Set up rainbow color provider
var rainbow_provider = animation.rich_palette(engine)
rainbow_provider.colors = animation.PALETTE_RAINBOW
rainbow_provider.period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
anim.color = rainbow_provider
anim.scale = 30
anim.speed = 20
anim.octaves = 3
anim.persistence = 128
return anim
end
return {'noise_animation': NoiseAnimation, 'noise_rainbow': noise_rainbow, 'noise_single_color': noise_single_color, 'noise_fractal': noise_fractal}

View File

@ -1,232 +0,0 @@
# Test suite for NoiseAnimation
#
# This test verifies that the NoiseAnimation works correctly
# with different parameters and color providers.
import animation
import string
# Test basic NoiseAnimation creation and functionality
def test_noise_animation_basic()
print("Testing basic NoiseAnimation...")
# Create LED strip and engine
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
# Test with default parameters
var noise_anim = animation.noise_animation(engine)
assert(noise_anim != nil, "NoiseAnimation should be created")
assert(noise_anim.scale == 50, "Default scale should be 50")
assert(noise_anim.speed == 30, "Default speed should be 30")
assert(noise_anim.octaves == 1, "Default octaves should be 1")
assert(noise_anim.is_running == false, "Animation should not be running initially")
print("✓ Basic NoiseAnimation test passed")
end
# Test NoiseAnimation with custom parameters
def test_noise_animation_custom()
print("Testing NoiseAnimation with custom parameters...")
# Create LED strip and engine
var strip = global.Leds(20)
var engine = animation.create_engine(strip)
# Test with custom parameters
var noise_anim = animation.noise_animation(engine)
noise_anim.color = 0xFF00FF00
noise_anim.scale = 100
noise_anim.speed = 80
noise_anim.octaves = 2
noise_anim.persistence = 200
noise_anim.seed = 12345
noise_anim.priority = 15
noise_anim.duration = 5000
noise_anim.loop = false
assert(noise_anim.scale == 100, "Custom scale should be 100")
assert(noise_anim.speed == 80, "Custom speed should be 80")
assert(noise_anim.octaves == 2, "Custom octaves should be 2")
assert(noise_anim.persistence == 200, "Custom persistence should be 200")
assert(noise_anim.seed == 12345, "Custom seed should be 12345")
assert(noise_anim.priority == 15, "Custom priority should be 15")
assert(noise_anim.duration == 5000, "Custom duration should be 5000")
assert(noise_anim.loop == false, "Custom loop should be false")
print("✓ Custom NoiseAnimation test passed")
end
# Test NoiseAnimation parameter changes
def test_noise_animation_parameters()
print("Testing NoiseAnimation parameter changes...")
# Create LED strip and engine
var strip = global.Leds(15)
var engine = animation.create_engine(strip)
var noise_anim = animation.noise_animation(engine)
# Test parameter changes via virtual member assignment
noise_anim.scale = 75
assert(noise_anim.scale == 75, "Scale should be updated to 75")
noise_anim.speed = 120
assert(noise_anim.speed == 120, "Speed should be updated to 120")
noise_anim.octaves = 3
assert(noise_anim.octaves == 3, "Octaves should be updated to 3")
# Test that current_colors array adapts to engine strip length
var initial_size = size(noise_anim.current_colors)
assert(initial_size == 15, "Current colors array should match engine strip length")
print("✓ NoiseAnimation parameter test passed")
end
# Test NoiseAnimation update and render
def test_noise_animation_update_render()
print("Testing NoiseAnimation update and render...")
# Create LED strip and engine
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var noise_anim = animation.noise_animation(engine)
noise_anim.color = 0xFFFF0000
noise_anim.scale = 60
noise_anim.speed = 40
var frame = animation.frame_buffer(10)
# Start animation
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
noise_anim.start_time = 1000 # Set start_time manually for direct testing
noise_anim.start(1000)
assert(noise_anim.is_running == true, "Animation should be running after start")
# Test update
noise_anim.update(1500)
assert(noise_anim.is_running == true, "Animation should still be running after update")
# Test render
var result = noise_anim.render(frame, 1500, engine.strip_length)
assert(result == true, "Render should return true for running animation")
# Check that colors were set (should not all be black)
var has_non_black = false
var i = 0
while i < frame.width
if frame.get_pixel_color(i) != 0xFF000000
has_non_black = true
break
end
i += 1
end
assert(has_non_black == true, "Frame should have non-black pixels after render")
print("✓ NoiseAnimation update/render test passed")
end
# Test global constructor functions
def test_noise_constructors()
print("Testing noise constructor functions...")
# Create LED strip and engine
var strip = global.Leds(15)
var engine = animation.create_engine(strip)
# Test noise_rainbow
var rainbow_noise = animation.noise_rainbow(engine)
assert(rainbow_noise != nil, "noise_rainbow should create animation")
assert(rainbow_noise.scale == 50, "Rainbow noise should have correct scale")
assert(rainbow_noise.speed == 30, "Rainbow noise should have correct speed")
assert(rainbow_noise.octaves == 1, "Rainbow noise should have correct octaves")
# Test noise_single_color
var single_noise = animation.noise_single_color(engine)
assert(single_noise != nil, "noise_single_color should create animation")
assert(single_noise.scale == 50, "Single color noise should have correct scale")
assert(single_noise.speed == 30, "Single color noise should have correct speed")
assert(single_noise.color == 0xFFFFFFFF, "Single color noise should have white color")
# Test noise_fractal
var fractal_noise = animation.noise_fractal(engine)
assert(fractal_noise != nil, "noise_fractal should create animation")
assert(fractal_noise.scale == 30, "Fractal noise should have correct scale")
assert(fractal_noise.octaves == 3, "Fractal noise should have correct octaves")
print("✓ Noise constructor functions test passed")
end
# Test NoiseAnimation string representation
def test_noise_tostring()
print("Testing NoiseAnimation string representation...")
# Create LED strip and engine
var strip = global.Leds(12)
var engine = animation.create_engine(strip)
var noise_anim = animation.noise_animation(engine)
noise_anim.scale = 75
noise_anim.speed = 45
noise_anim.octaves = 2
noise_anim.persistence = 150
var str_repr = str(noise_anim)
assert(type(str_repr) == "string", "String representation should be a string")
print("✓ NoiseAnimation string representation test passed")
end
# Test integer color conversion to gradient
def test_noise_integer_color_conversion()
print("Testing NoiseAnimation integer color conversion...")
# Create LED strip and engine
var strip = global.Leds(5)
var engine = animation.create_engine(strip)
var noise_anim = animation.noise_animation(engine)
# Set an integer color - should be converted to gradient provider
noise_anim.color = 0xFFFF0000 # Red
# Check the raw parameter value (should be a color provider)
var raw_color = noise_anim.get_param("color")
# Test that the raw parameter is a color provider (the conversion worked)
assert(animation.is_color_provider(raw_color), "Integer color should be converted to color provider")
print("✓ NoiseAnimation integer color conversion test passed")
end
# Run all tests
def run_noise_animation_tests()
print("=== NoiseAnimation Tests ===")
try
test_noise_animation_basic()
test_noise_animation_custom()
test_noise_animation_parameters()
test_noise_animation_update_render()
test_noise_constructors()
test_noise_tostring()
test_noise_integer_color_conversion()
print("=== All NoiseAnimation tests passed! ===")
return true
except .. as e, msg
print(f"Test failed: {e} - {msg}")
raise "test_failed"
end
end
# Export the test function
animation.run_noise_animation_tests = run_noise_animation_tests
run_noise_animation_tests()
return run_noise_animation_tests

View File

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

View File

@ -0,0 +1,138 @@
# Test for beacon animation right_edge parameter
# Tests that right_edge=1 positions the beacon using pos as the right edge of the beacon
import animation
import global
# Test counter
var test_count = 0
var passed_count = 0
# Create LED strip and engine for testing
var strip = global.Leds(10) # Use built-in LED strip for testing
var engine = animation.create_engine(strip)
def test_assert(condition, message)
test_count += 1
if condition
passed_count += 1
print(f"✓ Test {test_count}: {message}")
else
print(f"✗ Test {test_count}: {message}")
end
end
def run_tests()
print("Running Beacon Animation right_edge Tests...")
print("==================================================")
var strip_length = 10
var frame = animation.frame_buffer(strip_length)
# Test 1: right_edge=0 (default, left edge), pos=0, beacon_size=1
# Should show 1 pixel at position 0 (left edge)
var beacon1 = animation.beacon_animation(engine)
beacon1.color = 0xFFFF0000 # Red
beacon1.back_color = 0xFF000000 # Black
beacon1.pos = 0
beacon1.beacon_size = 1
beacon1.slew_size = 0
beacon1.right_edge = 0
beacon1.start()
frame.fill_pixels(frame.pixels, 0xFF000000) # Clear to black
beacon1.render(frame, 0, strip_length)
# Check pixel 0 is red, others are black
test_assert(frame.get_pixel_color(0) == 0xFFFF0000, "right_edge=0, pos=0: pixel 0 is red")
test_assert(frame.get_pixel_color(1) == 0xFF000000, "right_edge=0, pos=0: pixel 1 is black")
test_assert(frame.get_pixel_color(9) == 0xFF000000, "right_edge=0, pos=0: pixel 9 is black")
# Test 2: right_edge=1 (right edge), pos=0, beacon_size=1
# With right_edge=1: effective_pos = pos - beacon_size + 1 = 0 - 1 + 1 = 0
# So pixel 0 should be lit
var beacon2 = animation.beacon_animation(engine)
beacon2.color = 0xFF00FF00 # Green
beacon2.back_color = 0xFF000000 # Black
beacon2.pos = 0
beacon2.beacon_size = 1
beacon2.slew_size = 0
beacon2.right_edge = 1
beacon2.start()
frame.fill_pixels(frame.pixels, 0xFF000000) # Clear to black
beacon2.render(frame, 0, strip_length)
# Check pixel 0 is green (right edge of beacon at pos=0)
test_assert(frame.get_pixel_color(0) == 0xFF00FF00, "right_edge=1, pos=0: pixel 0 is green")
test_assert(frame.get_pixel_color(1) == 0xFF000000, "right_edge=1, pos=0: pixel 1 is black")
# Test 3: right_edge=1, pos=5, beacon_size=3
# With right_edge=1: effective_pos = pos - beacon_size + 1 = 5 - 3 + 1 = 3
# So pixels 3,4,5 should be lit (beacon from 3 to 5, with right edge at 5)
var beacon3 = animation.beacon_animation(engine)
beacon3.color = 0xFF0000FF # Blue
beacon3.back_color = 0xFF000000 # Black
beacon3.pos = 5
beacon3.beacon_size = 3
beacon3.slew_size = 0
beacon3.right_edge = 1
beacon3.start()
frame.fill_pixels(frame.pixels, 0xFF000000) # Clear to black
beacon3.render(frame, 0, strip_length)
test_assert(frame.get_pixel_color(2) == 0xFF000000, "right_edge=1, pos=5, size=3: pixel 2 is black")
test_assert(frame.get_pixel_color(3) == 0xFF0000FF, "right_edge=1, pos=5, size=3: pixel 3 is blue")
test_assert(frame.get_pixel_color(4) == 0xFF0000FF, "right_edge=1, pos=5, size=3: pixel 4 is blue")
test_assert(frame.get_pixel_color(5) == 0xFF0000FF, "right_edge=1, pos=5, size=3: pixel 5 is blue")
test_assert(frame.get_pixel_color(6) == 0xFF000000, "right_edge=1, pos=5, size=3: pixel 6 is black")
# Test 4: right_edge=0, pos=2, beacon_size=3 (same params, different right_edge)
# Should show pixels 2,3,4 lit
var beacon4 = animation.beacon_animation(engine)
beacon4.color = 0xFFFFFF00 # Yellow
beacon4.back_color = 0xFF000000 # Black
beacon4.pos = 2
beacon4.beacon_size = 3
beacon4.slew_size = 0
beacon4.right_edge = 0
beacon4.start()
frame.fill_pixels(frame.pixels, 0xFF000000) # Clear to black
beacon4.render(frame, 0, strip_length)
test_assert(frame.get_pixel_color(1) == 0xFF000000, "right_edge=0, pos=2, size=3: pixel 1 is black")
test_assert(frame.get_pixel_color(2) == 0xFFFFFF00, "right_edge=0, pos=2, size=3: pixel 2 is yellow")
test_assert(frame.get_pixel_color(3) == 0xFFFFFF00, "right_edge=0, pos=2, size=3: pixel 3 is yellow")
test_assert(frame.get_pixel_color(4) == 0xFFFFFF00, "right_edge=0, pos=2, size=3: pixel 4 is yellow")
test_assert(frame.get_pixel_color(5) == 0xFF000000, "right_edge=0, pos=2, size=3: pixel 5 is black")
# Test 5: Default right_edge should be 0
var beacon5 = animation.beacon_animation(engine)
test_assert(beacon5.right_edge == 0, "Default right_edge is 0")
# Test 6: Invalid right_edge value should be rejected
try
beacon5.right_edge = 2
test_assert(false, "Invalid right_edge value should be rejected")
except "value_error"
test_assert(true, "Invalid right_edge value properly rejected")
end
print("==================================================")
print(f"Tests completed: {passed_count}/{test_count} passed")
if passed_count == test_count
print("🎉 All tests passed!")
return true
else
print(f"❌ {test_count - passed_count} tests failed")
raise "test_failed"
end
end
# Run the tests
var success = run_tests()
return success