Berry animation simplify gradient (#24290)

This commit is contained in:
s-hadinger 2026-01-02 12:37:26 +01:00 committed by GitHub
parent 5967b4401c
commit f5d8ec43fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 11474 additions and 12476 deletions

View File

@ -22,13 +22,14 @@ ParameterizedObject (base class with parameter management and playable interface
│ ├── BeaconAnimation (pulse at specific position)
│ ├── CrenelPositionAnimation (crenel/square wave pattern)
│ ├── BreatheAnimation (breathing effect)
│ ├── BeaconAnimation (pulse at specific position)
│ │ └── GradientAnimation (linear/radial color gradients)
│ ├── PaletteGradientAnimation (gradient patterns with palette colors)
│ │ ├── PaletteMeterAnimation (meter/bar patterns)
│ │ └── GradientMeterAnimation (VU meter with gradient colors and peak hold)
│ ├── CometAnimation (moving comet with tail)
│ ├── FireAnimation (realistic fire effect)
│ ├── TwinkleAnimation (twinkling stars effect)
│ ├── GradientAnimation (color gradients)
│ ├── NoiseAnimation (Perlin noise patterns)
│ ├── WaveAnimation (wave motion effects)
│ └── RichPaletteAnimation (smooth palette transitions)
@ -590,19 +591,25 @@ Creates a realistic fire effect with flickering flames. Inherits from `Animation
### GradientAnimation
Creates smooth color gradients that can be linear or radial. Inherits from `Animation`.
Creates smooth two-color gradients. Subclass of `BeaconAnimation` that uses beacon slew regions to create gradient effects.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color` | instance | nil | nillable | Color provider (nil = rainbow gradient) |
| `gradient_type` | int | 0 | 0-1 | 0=linear, 1=radial |
| `direction` | int | 0 | 0-255 | Gradient direction/orientation |
| `center_pos` | int | 128 | 0-255 | Center position for radial gradients |
| `spread` | int | 255 | 1-255 | Gradient spread/compression |
| `movement_speed` | int | 0 | 0-255 | Speed of gradient movement |
| *(inherits all Animation parameters)* | | | | |
| `color1` | int | 0xFFFF0000 | - | First color (default red) |
| `color2` | int | 0xFF0000FF | - | Second color (default blue) |
| `direction` | int | 0 | enum: [0, 1] | 0=forward (color1→color2), 1=reverse (color2→color1) |
| `gradient_type` | int | 0 | enum: [0, 1] | 0=linear, 1=radial |
| *(inherits all BeaconAnimation parameters)* | | | | |
**Factories**: `animation.gradient_animation(engine)`, `animation.gradient_rainbow_linear(engine)`, `animation.gradient_rainbow_radial(engine)`, `animation.gradient_two_color_linear(engine)`
**Gradient Types:**
- **Linear (0)**: Creates a 2-color gradient from `color1` to `color2` (or reversed if `direction=1`). Implemented as the left slew of a large beacon positioned at the right edge.
- **Radial (1)**: Creates a symmetric gradient with `color1` at center and `color2` at edges (or reversed if `direction=1`). Implemented as a centered beacon with size=1 and slew regions extending to the edges.
**Implementation Details:**
- Linear gradient uses a beacon with `beacon_size=1000` (off-screen) and `slew_size=strip_length`
- Radial gradient uses a centered beacon with `beacon_size=1` and `slew_size=strip_length/2`
**Factory**: `animation.gradient_animation(engine)`
### GradientMeterAnimation
@ -737,6 +744,7 @@ Creates a pulse effect at a specific position with optional fade regions. Inheri
#### Visual Pattern
**right_edge=0 (default, left edge):**
```
pos (1)
|
@ -748,8 +756,20 @@ Creates a pulse effect at a specific position with optional fade regions. Inheri
|2| 3 |2|
```
**right_edge=1 (right edge):**
```
pos (1)
|
v
_______ |
/ \ |
_______/ \______|__
| | | |
|2| 3 |2|
```
Where:
1. `pos` - Start of the pulse (in pixels)
1. `pos` - Position of the beacon edge (left edge for right_edge=0, right edge for right_edge=1)
2. `slew_size` - Number of pixels to fade from back to fore color (can be 0)
3. `beacon_size` - Number of pixels of the pulse
@ -764,29 +784,52 @@ The pulse consists of:
|-----------|------|---------|-------------|-------------|
| `color` | int | 0xFFFFFFFF | - | Pulse color in ARGB format |
| `back_color` | int | 0xFF000000 | - | Background color in ARGB format |
| `pos` | int | 0 | - | Pulse start position in pixels |
| `pos` | int | 0 | - | Beacon edge position (left edge for right_edge=0, right edge for right_edge=1) |
| `beacon_size` | int | 1 | min: 0 | Size of core pulse in pixels |
| `slew_size` | int | 0 | min: 0 | Fade region size on each side in pixels |
| `right_edge` | int | 0 | enum: [0, 1] | 0=left edge (default), 1=right edge |
| *(inherits all Animation parameters)* | | | | |
#### right_edge Behavior
- **right_edge=0 (default)**: `pos` specifies the left edge of the beacon. `pos=0` places the beacon starting at the leftmost pixel.
- **right_edge=1**: `pos` specifies the right edge of the beacon from the right side of the strip. `pos=0` places the beacon's right edge at the rightmost pixel.
The effective left position is calculated as:
- `right_edge=0`: `effective_pos = pos`
- `right_edge=1`: `effective_pos = strip_length - pos - beacon_size`
#### Pattern Behavior
- **Sharp Pulse** (`slew_size = 0`): Rectangular pulse with hard edges
- **Soft Pulse** (`slew_size > 0`): Pulse with smooth fade-in/fade-out regions
- **Positioning**: `pos` defines the start of the core pulse region
- **Positioning**: `pos` defines the beacon edge from the specified side
- **Fade Calculation**: Linear fade from full brightness to background color
- **Boundary Handling**: Fade regions are clipped to frame boundaries
#### Usage Examples
```berry
# Sharp pulse at center
animation sharp_pulse = beacon_animation(
# Sharp pulse at left edge (right_edge=0, default)
animation left_pulse = beacon_animation(
color=red,
pos=10,
pos=0,
beacon_size=3,
slew_size=0
slew_size=0,
right_edge=0
)
# Shows 3 red pixels at positions 0, 1, 2
# Pulse from right edge
animation right_pulse = beacon_animation(
color=blue,
pos=0,
beacon_size=3,
slew_size=0,
right_edge=1
)
# With pos=0 and right_edge=1, shows 3 pixels at the right edge
# (positions strip_length-3, strip_length-2, strip_length-1)
# Soft pulse with fade regions
animation soft_pulse = beacon_animation(
@ -847,6 +890,30 @@ animation breathing_spot = beacon_animation(
breathing_spot.opacity = smooth(min_value=50, max_value=255, period=2s)
```
**Bidirectional Animations:**
```berry
# Two beacons moving from opposite edges toward center
set strip_len = strip_length()
set sweep = triangle(min_value=0, max_value=strip_len/2, period=2s)
animation left_beacon = beacon_animation(
color=red,
beacon_size=2,
right_edge=0
)
left_beacon.pos = sweep
animation right_beacon = beacon_animation(
color=blue,
beacon_size=2,
right_edge=1
)
right_beacon.pos = sweep
run left_beacon
run right_beacon
```
**Factory**: `animation.beacon_animation(engine)`
### CrenelPositionAnimation

View File

@ -3,7 +3,7 @@
# This animation creates a beacon effect at a specific position on the LED strip.
# It displays a color beacon with optional slew (fade) regions on both sides.
#
# Beacon diagram:
# Beacon diagram (right_edge=0, default, left edge):
# pos (1)
# |
# v
@ -13,9 +13,20 @@
# | | | |
# |2| 3 |2|
#
# 1: `pos`, start of the beacon (in pixel)
# Beacon diagram (right_edge=1, right edge):
# pos (1)
# |
# v
# _______
# / \
# _______/ \_________
# | | | |
# |2| 3 |2|
#
# 1: `pos`, position of the beacon edge (left edge for right_edge=0, right edge for right_edge=1)
# 2: `slew_size`, number of pixels to fade from back to fore color, can be `0`
# 3: `beacon_size`, number of pixels of the beacon
# When right_edge=1, pos=0 shows 1 pixel at the right edge (rightmost pixel of strip)
import "./core/param_encoder" as encode_constraints
@ -28,7 +39,8 @@ class BeaconAnimation : animation.animation
"back_color": {"default": 0xFF000000},
"pos": {"default": 0},
"beacon_size": {"min": 0, "default": 1},
"slew_size": {"min": 0, "default": 0}
"slew_size": {"min": 0, "default": 0},
"right_edge": {"enum": [0, 1], "default": 0}
})
# Render the beacon to the provided frame buffer
@ -44,15 +56,28 @@ class BeaconAnimation : animation.animation
var slew_size = self.slew_size
var beacon_size = self.beacon_size
var color = self.color
var right_edge = self.right_edge
# Fill background if not transparent
if (back_color != 0xFF000000) && ((back_color & 0xFF000000) != 0x00)
frame.fill_pixels(frame.pixels, back_color)
end
# Calculate effective position based on right_edge
# right_edge=0: pos is the left edge of the beacon (default)
# right_edge=1: pos is the right edge of the beacon (from right side of strip)
var effective_pos
if right_edge == 1
# Right edge mode: pos indicates right edge of beacon from right side of strip
# effective_pos is the left edge of the beacon in absolute coordinates
effective_pos = pos - beacon_size + 1
else
effective_pos = pos
end
# Calculate beacon boundaries
var beacon_min = pos
var beacon_max = pos + beacon_size
var beacon_min = effective_pos
var beacon_max = effective_pos + beacon_size
# Clamp to frame boundaries
if beacon_min < 0
@ -65,17 +90,12 @@ class BeaconAnimation : animation.animation
# Draw the main beacon
frame.fill_pixels(frame.pixels, color, beacon_min, beacon_max)
var i
# var i = beacon_min
# while i < beacon_max
# frame.set_pixel_color(i, color)
# i += 1
# end
# Draw slew regions if slew_size > 0
if slew_size > 0
# Left slew (fade from background to beacon color)
var left_slew_min = pos - slew_size
var left_slew_max = pos
var left_slew_min = effective_pos - slew_size
var left_slew_max = effective_pos
if left_slew_min < 0
left_slew_min = 0
@ -87,15 +107,15 @@ class BeaconAnimation : animation.animation
i = left_slew_min
while i < left_slew_max
# Calculate blend factor - blend from 255 (back) to 0 (fore) like original
var blend_factor = tasmota.scale_int(i, pos - slew_size - 1, pos, 255, 0)
var blend_factor = tasmota.scale_int(i, effective_pos - slew_size - 1, effective_pos, 255, 0)
var blended_color = frame.blend_linear(back_color, color, blend_factor)
frame.set_pixel_color(i, blended_color)
i += 1
end
# Right slew (fade from beacon color to background)
var right_slew_min = pos + beacon_size
var right_slew_max = pos + beacon_size + slew_size
var right_slew_min = effective_pos + beacon_size
var right_slew_max = effective_pos + beacon_size + slew_size
if right_slew_min < 0
right_slew_min = 0
@ -107,7 +127,7 @@ class BeaconAnimation : animation.animation
i = right_slew_min
while i < right_slew_max
# Calculate blend factor - blend from 0 (fore) to 255 (back) like original
var blend_factor = tasmota.scale_int(i, pos + beacon_size - 1, pos + beacon_size + slew_size, 0, 255)
var blend_factor = tasmota.scale_int(i, effective_pos + beacon_size - 1, effective_pos + beacon_size + slew_size, 0, 255)
var blended_color = frame.blend_linear(back_color, color, blend_factor)
frame.set_pixel_color(i, blended_color)
i += 1

View File

@ -46,7 +46,7 @@ class BreatheAnimation : animation.animation
if name == "color"
# When color is set, update the breathe_provider's base_color
# but keep the breathe_provider as the actual color source for rendering
if type(value) == 'int' || animation.is_value_provider(value)
if type(value) == 'int'
self.breathe_provider.base_color = value
# Restore the breathe_provider as the color source (bypass on_param_changed)
self.values["color"] = self.breathe_provider

View File

@ -1,244 +1,63 @@
# Gradient animation effect for Berry Animation Framework
#
# This animation creates smooth color gradients that can be linear or radial,
# with optional movement and color transitions over time.
# Creates smooth color gradients between two colors.
# Reimplemented as a subclass of BeaconAnimation for simplicity.
#
# Parameters:
# - color1: First color (default: red 0xFFFF0000)
# - color2: Second color (default: blue 0xFF0000FF)
# - gradient_type: 0=linear (two-point), 1=radial (center-to-edges)
# - direction: 0=forward (color1 to color2), 1=reverse (color2 to color1)
#
# Implementation:
# - Linear gradient: Left slew of a large beacon positioned at the right edge
# - Radial gradient: Centered beacon of size=1 with slew_size=strip_length/2
import "./core/param_encoder" as encode_constraints
#@ solidify:GradientAnimation,weak
class GradientAnimation : animation.animation
# Non-parameter instance variables only
var current_colors # Array of current colors for each pixel
var phase_offset # Current phase offset for movement
# Parameter definitions following parameterized class specification
class GradientAnimation : animation.beacon_animation
# Parameter definitions - gradient-specific parameters
static var PARAMS = animation.enc_params({
"color": {"default": nil, "nillable": true},
"gradient_type": {"min": 0, "max": 1, "default": 0},
"direction": {"min": 0, "max": 255, "default": 0},
"center_pos": {"min": 0, "max": 255, "default": 128},
"spread": {"min": 1, "max": 255, "default": 255},
"movement_speed": {"min": 0, "max": 255, "default": 0}
"color1": {"default": 0xFFFF0000}, # First color (default red)
"color2": {"default": 0xFF0000FF}, # Second color (default blue)
"direction": {"enum": [0, 1], "default": 0}, # 0=forward, 1=reverse
"gradient_type": {"enum": [0, 1], "default": 0} # 0=linear, 1=radial
})
# Initialize a new Gradient animation
def init(engine)
# Call parent constructor with engine only
super(self).init(engine)
# Initialize non-parameter instance variables only
self.current_colors = []
self.phase_offset = 0
# Initialize with default strip length from engine
var strip_length = self.engine.strip_length
self.current_colors.resize(strip_length)
# Initialize colors to black
var i = 0
while i < strip_length
self.current_colors[i] = 0xFF000000
i += 1
end
end
# Handle parameter changes
def on_param_changed(name, value)
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.strip_length
if size(self.current_colors) != current_strip_length
self.current_colors.resize(current_strip_length)
var i = size(self.current_colors)
while i < current_strip_length
if i >= size(self.current_colors) || self.current_colors[i] == nil
if i < size(self.current_colors)
self.current_colors[i] = 0xFF000000
end
end
i += 1
end
end
end
# Update animation state
def update(time_ms)
super(self).update(time_ms)
# Cache parameter values for performance
var movement_speed = self.movement_speed
# Update movement phase if movement is enabled
if movement_speed > 0
var elapsed = time_ms - self.start_time
# Movement speed: 0-255 maps to 0-10 cycles per second
var cycles_per_second = tasmota.scale_uint(movement_speed, 0, 255, 0, 10)
if cycles_per_second > 0
self.phase_offset = (elapsed * cycles_per_second / 1000) % 256
end
end
# Calculate gradient colors
self._calculate_gradient(time_ms)
end
# Calculate gradient colors for all pixels
def _calculate_gradient(time_ms)
# Cache parameter values for performance
var gradient_type = self.gradient_type
var color_param = self.color
var strip_length = self.engine.strip_length
# Ensure current_colors array matches strip length
if size(self.current_colors) != strip_length
self.current_colors.resize(strip_length)
end
var i = 0
while i < strip_length
var gradient_pos = 0
if gradient_type == 0
# Linear gradient
gradient_pos = self._calculate_linear_position(i, strip_length)
else
# Radial gradient
gradient_pos = self._calculate_radial_position(i, strip_length)
end
# Apply movement offset
gradient_pos = (gradient_pos + self.phase_offset) % 256
# Get color from provider
var color = 0xFF000000
# Handle default rainbow gradient if color is nil
if color_param == nil
# Create default rainbow gradient on-the-fly
var hue = tasmota.scale_uint(gradient_pos, 0, 255, 0, 359)
import light_state
var ls = light_state(3) # Create RGB light state
ls.HsToRgb(hue, 255) # Convert HSV to RGB
color = 0xFF000000 | (ls.r << 16) | (ls.g << 8) | ls.b
elif animation.is_color_provider(color_param) && color_param.get_color_for_value != nil
color = color_param.get_color_for_value(gradient_pos, 0)
elif animation.is_value_provider(color_param)
# Use resolve_value with position influence
color = self.resolve_value(color_param, "color", time_ms + gradient_pos * 10)
elif type(color_param) == "int"
# Single color - create gradient from black to color
var intensity = gradient_pos
var r = tasmota.scale_uint(intensity, 0, 255, 0, (color_param >> 16) & 0xFF)
var g = tasmota.scale_uint(intensity, 0, 255, 0, (color_param >> 8) & 0xFF)
var b = tasmota.scale_uint(intensity, 0, 255, 0, color_param & 0xFF)
color = 0xFF000000 | (r << 16) | (g << 8) | b
else
color = color_param
end
self.current_colors[i] = color
i += 1
end
end
# Calculate position for linear gradient
def _calculate_linear_position(pixel, strip_length)
var strip_pos = tasmota.scale_uint(pixel, 0, strip_length - 1, 0, 255)
# Cache parameter values
var direction = self.direction
var spread = self.spread
# Apply direction (0=left-to-right, 128=center-out, 255=right-to-left)
if direction <= 128
# Forward direction with varying start point
var start_offset = tasmota.scale_uint(direction, 0, 128, 0, 128)
strip_pos = (strip_pos + start_offset) % 256
else
# Reverse direction
var reverse_amount = tasmota.scale_uint(direction, 128, 255, 0, 255)
strip_pos = 255 - ((strip_pos + reverse_amount) % 256)
end
# Apply spread (compress or expand the gradient)
strip_pos = tasmota.scale_uint(strip_pos, 0, 255, 0, spread)
return strip_pos
end
# Calculate position for radial gradient
def _calculate_radial_position(pixel, strip_length)
var strip_pos = tasmota.scale_uint(pixel, 0, strip_length - 1, 0, 255)
# Cache parameter values
var center = self.center_pos
var spread = self.spread
# Calculate distance from center
var distance = 0
if strip_pos >= center
distance = strip_pos - center
else
distance = center - strip_pos
end
# Scale distance by spread
distance = tasmota.scale_uint(distance, 0, 128, 0, spread)
if distance > 255
distance = 255
end
return distance
end
# Render gradient to frame buffer
# Override render to dynamically configure beacon based on strip_length and gradient parameters
def render(frame, time_ms, strip_length)
var i = 0
while i < strip_length && i < frame.width
if i < size(self.current_colors)
frame.set_pixel_color(i, self.current_colors[i])
end
i += 1
var col1 = self.color1
var col2 = self.color2
var direction = self.direction
var gradient_type = self.gradient_type
if direction
self.color = col2
self.back_color = col1
else
self.color = col1
self.back_color = col2
end
return true
if gradient_type
# Radial gradient: centered beacon, color at center, back_color at edges
var center = (strip_length - 1) / 2
self.pos = center
self.beacon_size = 1 + (1 - strip_length & 1)
self.slew_size = (center > 0) ? center - 1 : 0
self.right_edge = 0
else
# Linear gradient: right slew of a large beacon at left edge
self.pos = 0
self.beacon_size = 1000
self.slew_size = (strip_length > 1) ? strip_length - 2 : 0
self.right_edge = 1
end
return super(self).render(frame, time_ms, strip_length)
end
end
# Factory functions following parameterized class specification
# Create a rainbow linear gradient
def gradient_rainbow_linear(engine)
var anim = animation.gradient_animation(engine)
anim.color = nil # Default rainbow
anim.gradient_type = 0 # Linear
anim.direction = 0 # Left-to-right
anim.movement_speed = 50 # Medium movement
return anim
end
# Create a rainbow radial gradient
def gradient_rainbow_radial(engine)
var anim = animation.gradient_animation(engine)
anim.color = nil # Default rainbow
anim.gradient_type = 1 # Radial
anim.center_pos = 128 # Center
anim.movement_speed = 30 # Slow movement
return anim
end
# Create a two-color linear gradient
def gradient_two_color_linear(engine)
var anim = animation.gradient_animation(engine)
anim.color = 0xFFFF0000 # Default red gradient
anim.gradient_type = 0 # Linear
anim.direction = 0 # Left-to-right
anim.movement_speed = 0 # Static
return anim
end
return {'gradient_animation': GradientAnimation,
'gradient_rainbow_linear': gradient_rainbow_linear,
'gradient_rainbow_radial': gradient_rainbow_radial,
'gradient_two_color_linear': gradient_two_color_linear}
return {
'gradient_animation': GradientAnimation
}

View File

@ -19,7 +19,7 @@ class RichPaletteAnimation : animation.animation
# RichPaletteColorProvider parameters (forwarded to internal provider)
"colors": {"type": "instance", "default": nil},
"period": {"min": 0, "default": 5000},
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.SINE},
"transition_type": {"enum": [1 #-LINEAR-#, 5 #-SINE-#], "default": 5 #-SINE-#},
"brightness": {"min": 0, "max": 255, "default": 255}
})

View File

@ -444,7 +444,7 @@ class AnimationEngine
# var cpu_percent = (self.tick_time_sum * 100) / period_ms
# Format and log stats - split into animation calc vs hardware output
var stats_msg = f"AnimEngine: ticks={self.tick_count} total={mean_time:.2f}ms({self.tick_time_min}-{self.tick_time_max}) events={mean_phase1:.2f}ms({self.phase1_time_min}-{self.phase1_time_max}) update={mean_phase2:.2f}ms({self.phase2_time_min}-{self.phase2_time_max}) anim={mean_anim:.2f}ms({self.anim_time_min}-{self.anim_time_max}) hw={mean_hw:.2f}ms({self.hw_time_min}-{self.hw_time_max})"
var stats_msg = f"ANI: ticks={self.tick_count} total={mean_time:.2f}ms({self.tick_time_min}-{self.tick_time_max}) events={mean_phase1:.2f}ms({self.phase1_time_min}-{self.phase1_time_max}) update={mean_phase2:.2f}ms({self.phase2_time_min}-{self.phase2_time_max}) anim={mean_anim:.2f}ms({self.anim_time_min}-{self.anim_time_max}) hw={mean_hw:.2f}ms({self.hw_time_min}-{self.hw_time_max})"
tasmota.log(stats_msg, 3) # Log level 3 (DEBUG)
end

View File

@ -31,7 +31,7 @@ class BreatheColorProvider : animation.oscillator_value
super(self).init(engine)
# Configure the inherited oscillator for breathing behavior
self.form = animation.COSINE # Use cosine wave for smooth breathing
self.form = 4 #-animation.COSINE-# # Use cosine wave for smooth breathing
self.min_value = 0 # Fixed range 0-255 for normalized oscillation
self.max_value = 255 # Fixed range 0-255 for normalized oscillation
self.duration = 3000 # Default duration
@ -43,11 +43,7 @@ class BreatheColorProvider : animation.oscillator_value
if name == "curve_factor"
# For curve_factor = 1, use pure cosine
# For curve_factor > 1, we'll apply the curve in produce_value
if value == 1
self.form = animation.COSINE
else
self.form = animation.COSINE # Still use cosine as base, apply curve later
end
self.form = 4 #-animation.COSINE-#
end
# Call parent's parameter change handler

View File

@ -28,9 +28,6 @@ class OscillatorValueProvider : animation.value_provider
# Non-parameter instance variables only
var value # current calculated value
# Static array for better solidification (moved from inline array)
static var form_names = ["", "SAWTOOTH", "TRIANGLE", "SQUARE", "COSINE", "SINE", "EASE_IN", "EASE_OUT", "ELASTIC", "BOUNCE"]
# Parameter definitions for the oscillator
static var PARAMS = animation.enc_params({
"min_value": {"default": 0},
@ -78,6 +75,8 @@ class OscillatorValueProvider : animation.value_provider
var form = self.form
var phase = self.phase
var duty_cycle = self.duty_cycle
var scale_uint = tasmota.scale_uint
var scale_int = tasmota.scale_int
# Ensure time_ms is valid and initialize start_time if needed
time_ms = self._fix_time_ms(time_ms)
@ -86,114 +85,80 @@ class OscillatorValueProvider : animation.value_provider
return min_value
end
# Calculate elapsed time since start_time
# Calculate elapsed time with cycle wrapping
var past = time_ms - self.start_time
if past < 0
past = 0
end
var duration_ms_mid = tasmota.scale_uint(duty_cycle, 0, 255, 0, duration)
# Handle cycle wrapping
if past >= duration
var cycles = past / duration
self.start_time += cycles * duration
self.start_time += (past / duration) * duration
past = past % duration
end
var past_with_phase = past
# Apply phase shift
if phase > 0
past_with_phase += tasmota.scale_uint(phase, 0, 255, 0, duration)
if past_with_phase >= duration
past_with_phase -= duration
past += scale_uint(phase, 0, 255, 0, duration)
if past >= duration
past -= duration
end
end
# Calculate value based on waveform
if form == animation.SAWTOOTH
self.value = tasmota.scale_int(past_with_phase, 0, duration - 1, min_value, max_value)
elif form == animation.TRIANGLE
if past_with_phase < duration_ms_mid
self.value = tasmota.scale_int(past_with_phase, 0, duration_ms_mid - 1, min_value, max_value)
# Compute normalized value (0-255) based on waveform, then scale to min/max
var v # normalized value 0-255
var duty_mid = scale_uint(duty_cycle, 0, 255, 0, duration)
if form == 3 #-SQUARE-#
self.value = past < duty_mid ? min_value : max_value
return self.value
elif form == 2 #-TRIANGLE-#
if past < duty_mid
v = scale_uint(past, 0, duty_mid - 1, 0, 255)
else
self.value = tasmota.scale_int(past_with_phase, duration_ms_mid, duration - 1, max_value, min_value)
v = scale_uint(past, duty_mid, duration - 1, 255, 0)
end
elif form == animation.SQUARE
if past_with_phase < duration_ms_mid
self.value = min_value
elif form == 4 || form == 5 #-COSINE/SINE-#
var angle = scale_uint(past, 0, duration - 1, 0, 32767)
if form == 4 angle -= 8192 end # cosine phase shift
v = scale_int(tasmota.sine_int(angle), -4096, 4096, 0, 255)
elif form == 6 || form == 7 #-EASE_IN/EASE_OUT-#
var t = scale_uint(past, 0, duration - 1, 0, 255)
if form == 6 # ease_in: t^2
v = scale_int(t * t, 0, 65025, 0, 255)
else # ease_out: 1-(1-t)^2
var inv = 255 - t
v = 255 - scale_int(inv * inv, 0, 65025, 0, 255)
end
elif form == 8 #-ELASTIC-#
var t = scale_uint(past, 0, duration - 1, 0, 255)
if t == 0 self.value = min_value return self.value end
if t == 255 self.value = max_value return self.value end
var decay = scale_uint(255 - t, 0, 255, 255, 32)
var osc = tasmota.sine_int(scale_uint(t, 0, 255, 0, 196602) % 32767)
var offset = scale_int(osc * decay, -1044480, 1044480, -255, 255)
self.value = min_value + scale_int(t, 0, 255, 0, max_value - min_value) + offset
# Clamp with 25% overshoot allowance
var overshoot = (max_value - min_value) / 4
if self.value > max_value + overshoot self.value = max_value + overshoot end
if self.value < min_value - overshoot self.value = min_value - overshoot end
return self.value
elif form == 9 #-BOUNCE-#
var t = scale_uint(past, 0, duration - 1, 0, 255)
if t < 128
var s = scale_uint(t, 0, 127, 0, 255)
v = 255 - scale_int((255-s)*(255-s), 0, 65025, 0, 255)
elif t < 192
var s = scale_uint(t - 128, 0, 63, 0, 255)
v = scale_int(255 - scale_int((255-s)*(255-s), 0, 65025, 0, 255), 0, 255, 0, 128)
else
self.value = max_value
var s = scale_uint(t - 192, 0, 63, 0, 255)
var bv = 255 - scale_int((255-s)*(255-s), 0, 65025, 0, 255)
v = 255 - scale_int(255 - bv, 0, 255, 0, 64)
end
elif form == animation.COSINE
# Map timing to 0..32767 for sine calculation
var angle = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 32767)
var x = tasmota.sine_int(angle - 8192) # -4096 .. 4096, dephase from cosine to sine
self.value = tasmota.scale_int(x, -4096, 4096, min_value, max_value)
elif form == animation.SINE
# Map timing to 0..32767 for sine calculation
var angle = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 32767)
var x = tasmota.sine_int(angle) # -4096 .. 4096, pure sine wave
self.value = tasmota.scale_int(x, -4096, 4096, min_value, max_value)
elif form == animation.EASE_IN
# Quadratic ease-in: starts slow, accelerates
var t = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 255) # 0..255
var eased = tasmota.scale_int(t * t, 0, 255 * 255, 0, 255) # t^2 scaled back to 0..255
self.value = tasmota.scale_int(eased, 0, 255, min_value, max_value)
elif form == animation.EASE_OUT
# Quadratic ease-out: starts fast, decelerates
var t = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 255) # 0..255
var inv_t = 255 - t
var eased = 255 - tasmota.scale_int(inv_t * inv_t, 0, 255 * 255, 0, 255) # 1 - (1-t)^2 scaled to 0..255
self.value = tasmota.scale_int(eased, 0, 255, min_value, max_value)
elif form == animation.ELASTIC
# Elastic easing: overshoots and oscillates like a spring
var t = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 255) # 0..255
if t == 0
self.value = min_value
elif t == 255
self.value = max_value
else
# Elastic formula: -2^(10*(t-1)) * sin((t-1-s)*2*pi/p) where s=p/4, p=0.3
# Simplified for integer math: amplitude decreases exponentially, frequency is high
var decay = tasmota.scale_uint(255 - t, 0, 255, 255, 32) # Exponential decay approximation
var freq_angle = tasmota.scale_uint(t, 0, 255, 0, 32767 * 6) # High frequency oscillation
var oscillation = tasmota.sine_int(freq_angle % 32767) # -4096 to 4096
var elastic_offset = tasmota.scale_int(oscillation * decay, -4096 * 255, 4096 * 255, -255, 255) # Scale oscillation by decay
var base_progress = tasmota.scale_int(t, 0, 255, 0, max_value - min_value)
self.value = min_value + base_progress + elastic_offset
# Clamp to reasonable bounds to prevent extreme overshoots
var value_range = max_value - min_value
var max_overshoot = tasmota.scale_int(value_range, 0, 4, 0, 1) # Allow 25% overshoot
if self.value > max_value + max_overshoot self.value = max_value + max_overshoot end
if self.value < min_value - max_overshoot self.value = min_value - max_overshoot end
end
elif form == animation.BOUNCE
# Bounce easing: like a ball bouncing with decreasing amplitude
var t = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 255) # 0..255
var bounced_t = 0
# Simplified bounce with 3 segments for better behavior
if t < 128 # First big bounce (0-50% of time)
var segment_t = tasmota.scale_uint(t, 0, 127, 0, 255)
var inv_segment = 255 - segment_t
bounced_t = 255 - tasmota.scale_int(inv_segment * inv_segment, 0, 255 * 255, 0, 255) # Ease-out curve
elif t < 192 # Second smaller bounce (50-75% of time)
var segment_t = tasmota.scale_uint(t - 128, 0, 63, 0, 255)
var inv_segment = 255 - segment_t
var bounce_val = 255 - tasmota.scale_int(inv_segment * inv_segment, 0, 255 * 255, 0, 255)
bounced_t = tasmota.scale_int(bounce_val, 0, 255, 0, 128) # Scale to 50% height
else # Final settle (75-100% of time)
var segment_t = tasmota.scale_uint(t - 192, 0, 63, 0, 255)
var inv_segment = 255 - segment_t
var bounce_val = 255 - tasmota.scale_int(inv_segment * inv_segment, 0, 255 * 255, 0, 255)
bounced_t = 255 - tasmota.scale_int(255 - bounce_val, 0, 255, 0, 64) # Settle towards full value
end
self.value = tasmota.scale_int(bounced_t, 0, 255, min_value, max_value)
else #-SAWTOOTH (default)-#
v = scale_uint(past, 0, duration - 1, 0, 255)
end
self.value = scale_int(v, 0, 255, min_value, max_value)
return self.value
end
end
@ -209,7 +174,7 @@ end
# @return OscillatorValueProvider - New ramp instance
def ramp(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.SAWTOOTH
osc.form = 1 #-animation.SAWTOOTH-#
return osc
end
@ -219,7 +184,7 @@ end
# @return OscillatorValueProvider - New linear oscillator instance
def linear(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.TRIANGLE
osc.form = 2 #-animation.TRIANGLE-#
return osc
end
@ -229,7 +194,7 @@ end
# @return OscillatorValueProvider - New smooth oscillator instance
def smooth(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.COSINE
osc.form = 4 #-animation.COSINE-#
return osc
end
@ -239,7 +204,7 @@ end
# @return OscillatorValueProvider - New cosine oscillator instance
def cosine_osc(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.COSINE
osc.form = 4 #-animation.COSINE-#
return osc
end
@ -249,7 +214,7 @@ end
# @return OscillatorValueProvider - New sine wave instance
def sine_osc(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.SINE
osc.form = 5 #-animation.SINE-#
return osc
end
@ -259,7 +224,7 @@ end
# @return OscillatorValueProvider - New square wave instance
def square(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.SQUARE
osc.form = 3 #-animation.SQUARE-#
return osc
end
@ -269,7 +234,7 @@ end
# @return OscillatorValueProvider - New ease-in instance
def ease_in(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.EASE_IN
osc.form = 6 #-animation.EASE_IN-#
return osc
end
@ -279,7 +244,7 @@ end
# @return OscillatorValueProvider - New ease-out instance
def ease_out(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.EASE_OUT
osc.form = 7 #-animation.EASE_OUT-#
return osc
end
@ -289,7 +254,7 @@ end
# @return OscillatorValueProvider - New elastic instance
def elastic(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.ELASTIC
osc.form = 8 #-animation.ELASTIC-#
return osc
end
@ -299,7 +264,7 @@ end
# @return OscillatorValueProvider - New bounce instance
def bounce(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.BOUNCE
osc.form = 9 #-animation.BOUNCE-#
return osc
end
@ -309,7 +274,7 @@ end
# @return OscillatorValueProvider - New sawtooth instance
def sawtooth(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.SAWTOOTH
osc.form = 1 #-animation.SAWTOOTH-#
return osc
end
@ -319,7 +284,7 @@ end
# @return OscillatorValueProvider - New triangle instance
def triangle(engine)
var osc = animation.oscillator_value(engine)
osc.form = animation.TRIANGLE
osc.form = 2 #-animation.TRIANGLE-#
return osc
end

View File

@ -44,7 +44,7 @@ class RichPaletteColorProvider : animation.color_provider
static var PARAMS = animation.enc_params({
"colors": {"type": "bytes", "default": nil}, # Palette bytes or predefined palette constant
"period": {"min": 0, "default": 5000}, # 5 seconds default, 0 = value-based only
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR}
"transition_type": {"enum": [1 #-animation.LINEAR-#, 5 #-animation.SINE-#], "default": 1 #-animation.LINEAR-#}
# brightness parameter inherited from ColorProvider base class
})
@ -212,7 +212,7 @@ class RichPaletteColorProvider : animation.color_provider
def _interpolate(value, from_min, from_max, to_min, to_max)
var transition_type = self.transition_type
if transition_type == animation.SINE
if transition_type == 5 #-animation.SINE-#
# Cosine interpolation for smooth transitions
# Map value to 0..255 range first
var t = tasmota.scale_uint(value, from_min, from_max, 0, 255)

File diff suppressed because it is too large Load Diff

View File

@ -50,15 +50,6 @@ red_breathe.period = 3000
red_breathe.curve_factor = 2
print(f"Red breathe animation color: 0x{red_breathe.breathe_provider.base_color :08x}")
# Create green breathe animation with color as a closure value provider
var green_breathe = animation.breathe_animation(engine)
green_breathe.color = animation.create_closure_value(engine, def (engine) return 0xFF00FF00 end)
green_breathe.min_brightness = 10
green_breathe.max_brightness = 180
green_breathe.period = 3000
green_breathe.curve_factor = 2
print(f"Green breathe animation color: {green_breathe.breathe_provider.base_color}")
# Test parameter updates using virtual member assignment
blue_breathe.min_brightness = 30
blue_breathe.max_brightness = 220
@ -142,8 +133,6 @@ print("✓ Animation added to engine successfully")
assert(anim != nil, "Default breathe animation should be created")
assert(blue_breathe != nil, "Custom breathe animation should be created")
assert(red_breathe != nil, "Red breathe animation should be created")
assert(green_breathe != nil, "Green breathe animation should be created")
assert(animation.is_value_provider(green_breathe.breathe_provider.base_color), "Green breathe should have color as a value provider")
assert(blue_breathe.breathe_provider.base_color == 0xFF0000FF, "Blue breathe should have correct color")
assert(blue_breathe.min_brightness == 30, "Min brightness should be updated to 30")
assert(blue_breathe.max_brightness == 220, "Max brightness should be updated to 220")

View File

@ -207,7 +207,7 @@ assert(blue_breathe.min_brightness == 30, "Min brightness should be updated to 3
assert(blue_breathe.max_brightness == 220, "Max brightness should be updated to 220")
assert(blue_breathe.duration == 3500, "Duration should be updated to 3500")
assert(blue_breathe.curve_factor == 4, "Curve factor should be updated to 4")
assert(blue_breathe.form == animation.COSINE, "Form should be COSINE")
assert(blue_breathe.form == 4 #-COSINE-#, "Form should be COSINE")
assert(blue_breathe.min_value == 0, "Inherited min_value should be 0")
assert(blue_breathe.max_value == 255, "Inherited max_value should be 255")
assert(blue_breathe.engine == engine, "Provider should have correct engine reference")

View File

@ -1,7 +1,7 @@
# Test suite for GradientAnimation
#
# This test verifies that the GradientAnimation works correctly
# with different gradient types, colors, and movement patterns.
# This test verifies that the simplified GradientAnimation works correctly
# with linear and radial gradients using beacon-based rendering.
import animation
@ -13,26 +13,19 @@ def test_gradient_creation()
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
# Test default gradient (rainbow linear)
# Test default gradient
var gradient = animation.gradient_animation(engine)
assert(gradient != nil, "Should create gradient animation")
assert(gradient.gradient_type == 0, "Should default to linear gradient")
assert(gradient.direction == 0, "Should default to left-to-right direction")
# Test single color gradient
var red_gradient = animation.gradient_animation(engine)
red_gradient.color = 0xFFFF0000
assert(red_gradient != nil, "Should create red gradient")
assert(gradient.direction == 0, "Should default to forward direction")
assert(gradient.color1 == 0xFFFF0000, "Should default to red color1")
assert(gradient.color2 == 0xFF0000FF, "Should default to blue color2")
# Test radial gradient
var radial_gradient = animation.gradient_animation(engine)
radial_gradient.gradient_type = 1
radial_gradient.center_pos = 64
radial_gradient.spread = 200
radial_gradient.movement_speed = 100
radial_gradient.priority = 10
radial_gradient.duration = 5000
radial_gradient.loop = false
radial_gradient.color1 = 0xFF000000
radial_gradient.color2 = 0xFFFFFFFF
assert(radial_gradient != nil, "Should create radial gradient")
assert(radial_gradient.gradient_type == 1, "Should be radial gradient")
@ -46,27 +39,23 @@ def test_gradient_parameters()
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var gradient = animation.gradient_animation(engine)
gradient.color = 0xFFFFFFFF
# Test parameter setting via virtual members
gradient.gradient_type = 1
assert(gradient.gradient_type == 1, "Should update gradient type")
gradient.direction = 128
assert(gradient.direction == 128, "Should update direction")
gradient.direction = 1
assert(gradient.direction == 1, "Should update direction")
gradient.center_pos = 200
assert(gradient.center_pos == 200, "Should update center position")
gradient.color1 = 0xFF0000FF
assert(gradient.color1 == 0xFF0000FF, "Should update color1")
gradient.spread = 128
assert(gradient.spread == 128, "Should update spread")
gradient.movement_speed = 150
assert(gradient.movement_speed == 150, "Should update movement speed")
gradient.color2 = 0xFFFF0000
assert(gradient.color2 == 0xFFFF0000, "Should update color2")
# Test parameter validation via set_param method
assert(gradient.set_param("gradient_type", 5) == false, "Should reject invalid gradient type")
assert(gradient.set_param("spread", 0) == false, "Should reject zero spread")
assert(gradient.set_param("direction", 5) == false, "Should reject invalid direction")
print("✓ GradientAnimation parameters test passed")
end
@ -78,28 +67,19 @@ def test_gradient_updates()
var strip = global.Leds(5)
var engine = animation.create_engine(strip)
var gradient = animation.gradient_animation(engine)
gradient.color = 0xFF00FF00
gradient.movement_speed = 100
gradient.color1 = 0xFF000000
gradient.color2 = 0xFF00FF00
# Start the animation
# Note: When testing animations directly (not through engine_proxy), we must set start_time manually
gradient.start_time = 1000 # Set start_time manually for direct testing
gradient.start_time = 1000
gradient.start(1000)
assert(gradient.is_running == true, "Should be running after start")
# Test update at different times
gradient.update(1000)
assert(gradient.is_running == true, "Should be running after update at start time")
assert(gradient.is_running == true, "Should be running after update")
gradient.update(1500)
assert(gradient.is_running == true, "Should be running after update at 500ms")
gradient.update(2000)
assert(gradient.is_running == true, "Should be running after update at 1000ms")
# Test that movement_speed affects phase_offset
var initial_offset = gradient.phase_offset
gradient.update(3000) # 2 seconds later
# With movement_speed=100, should have moved
# (movement is time-based, so offset should change)
print("✓ GradientAnimation updates test passed")
end
@ -111,14 +91,14 @@ def test_gradient_rendering()
var strip = global.Leds(5)
var engine = animation.create_engine(strip)
var gradient = animation.gradient_animation(engine)
gradient.color = 0xFFFF0000
gradient.movement_speed = 0
gradient.color1 = 0xFF000000 # Black
gradient.color2 = 0xFFFF0000 # Red
# Create a frame buffer
var frame = animation.frame_buffer(5, 1)
var frame = animation.frame_buffer(5)
# Start and update the animation
gradient.start_time = 1000 # Set start_time manually for direct testing
gradient.start_time = 1000
gradient.start(1000)
gradient.update(1000)
@ -127,181 +107,88 @@ def test_gradient_rendering()
assert(result == true, "Should render successfully")
# Test that colors were set (basic check)
# For a red gradient, pixels should have some red component
var first_color = frame.get_pixel_color(0)
var last_color = frame.get_pixel_color(4) # Last pixel in 5-pixel strip
# Colors should be different in a gradient
var last_color = frame.get_pixel_color(4)
# Colors should be different in a gradient (black to red)
assert(first_color != last_color, "First and last pixels should be different in gradient")
print("✓ GradientAnimation rendering test passed")
end
# Test gradient factory methods
def test_gradient_factory_methods()
print("Testing GradientAnimation factory methods...")
var strip = global.Leds(20)
var engine = animation.create_engine(strip)
# Test rainbow linear factory
var rainbow_linear = animation.gradient_rainbow_linear(engine)
assert(rainbow_linear != nil, "Should create rainbow linear gradient")
assert(rainbow_linear.gradient_type == 0, "Should be linear")
assert(rainbow_linear.movement_speed == 50, "Should set movement speed")
# Test rainbow radial factory
var rainbow_radial = animation.gradient_rainbow_radial(engine)
assert(rainbow_radial != nil, "Should create rainbow radial gradient")
assert(rainbow_radial.gradient_type == 1, "Should be radial")
assert(rainbow_radial.center_pos == 128, "Should set center position")
assert(rainbow_radial.movement_speed == 30, "Should set movement speed")
# Test two-color linear factory
var two_color = animation.gradient_two_color_linear(engine)
assert(two_color != nil, "Should create two-color gradient")
assert(two_color.gradient_type == 0, "Should be linear")
assert(two_color.movement_speed == 0, "Should set movement speed")
print("✓ GradientAnimation factory methods test passed")
end
# Test gradient position calculations
def test_gradient_position_calculations()
print("Testing GradientAnimation position calculations...")
# Test linear gradient direction
def test_gradient_direction()
print("Testing GradientAnimation direction...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
# Test linear gradient with different directions
var linear_gradient = animation.gradient_animation(engine)
linear_gradient.color = 0xFFFFFFFF
linear_gradient.movement_speed = 0
linear_gradient.start_time = 1000 # Set start_time manually for direct testing
linear_gradient.start(1000)
linear_gradient.update(1000)
# Test forward direction (color1 -> color2)
var forward_gradient = animation.gradient_animation(engine)
forward_gradient.color1 = 0xFF000000 # Black
forward_gradient.color2 = 0xFFFF0000 # Red
forward_gradient.direction = 0
forward_gradient.start_time = 1000
forward_gradient.start(1000)
forward_gradient.update(1000)
# The _calculate_linear_position method is private, but we can test the overall effect
# by checking that different pixels get different colors in a linear gradient
var frame = animation.frame_buffer(10, 1)
linear_gradient.render(frame, 1000, engine.strip_length)
var frame1 = animation.frame_buffer(10)
forward_gradient.render(frame1, 1000, engine.strip_length)
var forward_first = frame1.get_pixel_color(0)
var forward_last = frame1.get_pixel_color(9)
var first_color = frame.get_pixel_color(0)
var last_color = frame.get_pixel_color(9)
# In a gradient, first and last pixels should typically have different colors
# (unless it's a very specific case)
# Test reverse direction (color2 -> color1)
var reverse_gradient = animation.gradient_animation(engine)
reverse_gradient.color1 = 0xFF000000 # Black
reverse_gradient.color2 = 0xFFFF0000 # Red
reverse_gradient.direction = 1
reverse_gradient.start_time = 1000
reverse_gradient.start(1000)
reverse_gradient.update(1000)
var frame2 = animation.frame_buffer(10)
reverse_gradient.render(frame2, 1000, engine.strip_length)
var reverse_first = frame2.get_pixel_color(0)
var reverse_last = frame2.get_pixel_color(9)
# Forward: first should be darker (black), last should be brighter (red)
# Reverse: first should be brighter (red), last should be darker (black)
print(f" Forward: first=0x{forward_first:08X}, last=0x{forward_last:08X}")
print(f" Reverse: first=0x{reverse_first:08X}, last=0x{reverse_last:08X}")
print("✓ GradientAnimation direction test passed")
end
# Test radial gradient
def test_radial_gradient()
print("Testing GradientAnimation radial mode...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var radial_gradient = animation.gradient_animation(engine)
radial_gradient.color = 0xFFFFFFFF
radial_gradient.gradient_type = 1
radial_gradient.movement_speed = 0
radial_gradient.start_time = 1000 # Set start_time manually for direct testing
radial_gradient.gradient_type = 1 # Radial
radial_gradient.color1 = 0xFF000000 # Black at center
radial_gradient.color2 = 0xFFFFFFFF # White at edges
radial_gradient.start_time = 1000
radial_gradient.start(1000)
radial_gradient.update(1000)
var frame = animation.frame_buffer(10)
radial_gradient.render(frame, 1000, engine.strip_length)
# In a radial gradient, center pixel should be different from edge pixels
var center_color = frame.get_pixel_color(5) # Middle pixel
var edge_color = frame.get_pixel_color(0) # Edge pixel
# In radial gradient with direction=0, color1 at center, color2 at edges
var edge_color = frame.get_pixel_color(0)
var center_color = frame.get_pixel_color(5)
print("✓ GradientAnimation position calculations test passed")
end
print(f" Edge (0): 0x{edge_color:08X}")
print(f" Center (5): 0x{center_color:08X}")
# Test refactored color system
def test_gradient_color_refactoring()
print("Testing GradientAnimation color refactoring...")
# Edge should be brighter than center (white vs black)
var edge_brightness = ((edge_color >> 16) & 0xFF) + ((edge_color >> 8) & 0xFF) + (edge_color & 0xFF)
var center_brightness = ((center_color >> 16) & 0xFF) + ((center_color >> 8) & 0xFF) + (center_color & 0xFF)
assert(edge_brightness > center_brightness, "Edge should be brighter than center in radial gradient")
var strip = global.Leds(5)
var engine = animation.create_engine(strip)
# Test with static color
var static_gradient = animation.gradient_animation(engine)
static_gradient.color = 0xFFFF0000
assert(static_gradient.color == 0xFFFF0000, "Should have color set")
# Test with nil color (default rainbow)
var rainbow_gradient = animation.gradient_animation(engine)
rainbow_gradient.color = nil
assert(rainbow_gradient.color == nil, "Should accept nil color for rainbow")
# Test color resolution
var resolved_color = static_gradient.resolve_value(static_gradient.color, "color", 1000)
assert(resolved_color != nil, "Should resolve color")
# Test basic rendering with different color types
var frame = animation.frame_buffer(5, 1)
static_gradient.start_time = 1000 # Set start_time manually for direct testing
static_gradient.start(1000)
static_gradient.update(1000)
var result = static_gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render with static color")
rainbow_gradient.start_time = 1000 # Set start_time manually for direct testing
rainbow_gradient.start(1000)
rainbow_gradient.update(1000)
result = rainbow_gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render with rainbow color")
print("✓ GradientAnimation color refactoring test passed")
end
# Test virtual parameter access
def test_gradient_virtual_parameters()
print("Testing GradientAnimation virtual parameters...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
var gradient = animation.gradient_animation(engine)
# Test virtual parameter assignment and access
gradient.color = 0xFFFF00FF
assert(gradient.color == 0xFFFF00FF, "Should update color via virtual member")
gradient.gradient_type = 1
assert(gradient.gradient_type == 1, "Should update gradient type via virtual member")
gradient.direction = 200
assert(gradient.direction == 200, "Should update direction via virtual member")
gradient.center_pos = 64
assert(gradient.center_pos == 64, "Should update center position via virtual member")
gradient.spread = 128
assert(gradient.spread == 128, "Should update spread via virtual member")
gradient.movement_speed = 75
assert(gradient.movement_speed == 75, "Should update movement speed via virtual member")
print("✓ GradientAnimation virtual parameters test passed")
end
# Test updated tostring method
def test_gradient_tostring()
print("Testing GradientAnimation tostring...")
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
# Test with static color
var static_gradient = animation.gradient_animation(engine)
static_gradient.color = 0xFFFF0000
static_gradient.movement_speed = 50
var str_static = str(static_gradient)
assert(str_static != nil, "Should have string representation")
assert(type(str_static) == "string", "Should be a string")
# Test with color provider
var color_provider = animation.static_color(engine)
color_provider.color = 0xFF00FF00
var provider_gradient = animation.gradient_animation(engine)
provider_gradient.color = color_provider
provider_gradient.gradient_type = 1
provider_gradient.movement_speed = 25
var str_provider = str(provider_gradient)
assert(str_provider != nil, "Should have string representation")
assert(type(str_provider) == "string", "Should be a string")
print("✓ GradientAnimation tostring test passed")
print("✓ GradientAnimation radial mode test passed")
end
# Run all tests
@ -313,11 +200,8 @@ def run_gradient_animation_tests()
test_gradient_parameters()
test_gradient_updates()
test_gradient_rendering()
test_gradient_factory_methods()
test_gradient_position_calculations()
test_gradient_color_refactoring()
test_gradient_virtual_parameters()
test_gradient_tostring()
test_gradient_direction()
test_radial_gradient()
print("=== All GradientAnimation tests passed! ===")
return true

View File

@ -1,27 +1,29 @@
# Test for gradient rainbow functionality with light_state HSV conversion
# Test for gradient color variation
import animation
print("Testing gradient rainbow with light_state HSV conversion...")
print("Testing gradient color variation...")
# Create LED strip and engine
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
# Test rainbow gradient (nil color)
var rainbow_gradient = animation.gradient_animation(engine)
rainbow_gradient.color = nil # Should use rainbow
rainbow_gradient.movement_speed = 0 # Static for testing
# Test linear gradient with two colors
var gradient = animation.gradient_animation(engine)
gradient.color1 = 0xFF0000FF # Blue
gradient.color2 = 0xFFFF0000 # Red
gradient.gradient_type = 0 # Linear
gradient.direction = 0 # Forward (blue to red)
# Start and update
rainbow_gradient.start(1000)
rainbow_gradient.update(1000)
gradient.start(1000)
gradient.update(1000)
# Create frame and render
var frame = animation.frame_buffer(10, 1)
var result = rainbow_gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render rainbow gradient successfully")
var frame = animation.frame_buffer(10)
var result = gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render gradient successfully")
# Check that different pixels have different colors (rainbow effect)
# Check that different pixels have different colors (gradient effect)
var colors = []
var i = 0
while i < 10
@ -31,17 +33,10 @@ end
# Verify that we have some color variation (not all the same)
var first_color = colors[0]
var has_variation = false
i = 1
while i < size(colors)
if colors[i] != first_color
has_variation = true
break
end
i += 1
end
var last_color = colors[9]
var has_variation = first_color != last_color
assert(has_variation, "Rainbow gradient should have color variation across pixels")
assert(has_variation, "Gradient should have color variation across pixels")
# Test that colors have proper alpha channel (should be 0xFF)
i = 0
@ -51,6 +46,10 @@ while i < size(colors)
i += 1
end
print("✓ Gradient rainbow with light_state HSV conversion test passed!")
# Print colors for debugging
print(f" First pixel: 0x{first_color:08X}")
print(f" Last pixel: 0x{last_color:08X}")
print("✓ Gradient color variation test passed!")
return true

View File

@ -12,14 +12,16 @@ var gradient = animation.gradient_animation(engine)
assert(gradient != nil, "Should create gradient animation")
# Test parameter setting
gradient.color = 0xFFFF0000
gradient.color1 = 0xFF000000
gradient.color2 = 0xFFFF0000
gradient.gradient_type = 0
gradient.movement_speed = 50
gradient.direction = 0
# Test parameter access
assert(gradient.color == 0xFFFF0000, "Should set color")
assert(gradient.color1 == 0xFF000000, "Should set color1")
assert(gradient.color2 == 0xFFFF0000, "Should set color2")
assert(gradient.gradient_type == 0, "Should set gradient type")
assert(gradient.movement_speed == 50, "Should set movement speed")
assert(gradient.direction == 0, "Should set direction")
# Test start and update
gradient.start(1000)
@ -29,8 +31,8 @@ gradient.update(1000)
assert(gradient.is_running == true, "Should still be running after update")
# Test rendering
var frame = animation.frame_buffer(5, 1)
result = gradient.render(frame, 1000, engine.strip_length)
var frame = animation.frame_buffer(5)
var result = gradient.render(frame, 1000, engine.strip_length)
assert(result == true, "Should render successfully")
print("✓ Basic GradientAnimation test passed!")

View File

@ -45,17 +45,16 @@ success = test_obj.set_param("non_nillable_param", 100)
assert(success == true, "Should accept valid value for non-nillable parameter")
assert(test_obj.non_nillable_param == 100, "Should store valid value for non-nillable parameter")
# Test gradient animation nillable color parameter
# Test gradient animation color parameter
var gradient = animation.gradient_animation(engine)
# Test setting nil on gradient color (should work because it's nillable)
gradient.color = nil
assert(gradient.color == nil, "Should accept nil for nillable gradient color")
# Test setting a valid color (should work)
gradient.color = 0xFFFF0000
assert(gradient.color == 0xFFFF0000, "Should accept valid color for gradient")
# Note: The 'color' parameter behavior for nil depends on the base Animation class definition
# This test focuses on the custom nillable parameter attribute, not gradient-specific behavior
print("✓ Nillable parameter attribute test passed!")
return true

View File

@ -98,7 +98,7 @@ def test_ease_constructors()
assert(ease_in_provider.min_value == 10, "ease_in should set correct start value")
assert(ease_in_provider.max_value == 90, "ease_in should set correct end value")
assert(ease_in_provider.duration == 2000, "ease_in should set correct duration")
assert(ease_in_provider.form == animation.EASE_IN, "ease_in should set EASE_IN form")
assert(ease_in_provider.form == 6 #-EASE_IN-#, "ease_in should set EASE_IN form")
# Test ease_out constructor
var ease_out_provider = animation.ease_out(engine)
@ -108,7 +108,7 @@ def test_ease_constructors()
assert(ease_out_provider.min_value == 20, "ease_out should set correct start value")
assert(ease_out_provider.max_value == 80, "ease_out should set correct end value")
assert(ease_out_provider.duration == 1500, "ease_out should set correct duration")
assert(ease_out_provider.form == animation.EASE_OUT, "ease_out should set EASE_OUT form")
assert(ease_out_provider.form == 7 #-EASE_OUT-#, "ease_out should set EASE_OUT form")
print("✓ Ease constructor functions test passed")
end
@ -183,8 +183,8 @@ def test_ease_tostring()
ease_out_provider.duration = 2500
# Verify form values are set correctly
assert(ease_in_provider.form == animation.EASE_IN, "EASE_IN form should be set")
assert(ease_out_provider.form == animation.EASE_OUT, "EASE_OUT form should be set")
assert(ease_in_provider.form == 6 #-EASE_IN-#, "EASE_IN form should be set")
assert(ease_out_provider.form == 7 #-EASE_OUT-#, "EASE_OUT form should be set")
print("✓ Ease tostring test passed")
end
@ -198,16 +198,16 @@ def test_ease_constants()
direct_ease_in.min_value = 0
direct_ease_in.max_value = 100
direct_ease_in.duration = 1000
direct_ease_in.form = animation.EASE_IN
direct_ease_in.form = 6 #-EASE_IN-#
var direct_ease_out = animation.oscillator_value(engine)
direct_ease_out.min_value = 0
direct_ease_out.max_value = 100
direct_ease_out.duration = 1000
direct_ease_out.form = animation.EASE_OUT
direct_ease_out.form = 7 #-EASE_OUT-#
assert(direct_ease_in.form == animation.EASE_IN, "Direct EASE_IN should work")
assert(direct_ease_out.form == animation.EASE_OUT, "Direct EASE_OUT should work")
assert(direct_ease_in.form == 6 #-EASE_IN-#, "Direct EASE_IN should work")
assert(direct_ease_out.form == 7 #-EASE_OUT-#, "Direct EASE_OUT should work")
print("✓ Ease constants test passed")
end

View File

@ -163,7 +163,7 @@ rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new palette: lut_dirty = {rebuild_provider._lut_dirty}")
# Change transition_type - SHOULD trigger rebuild
rebuild_provider.transition_type = animation.SINE
rebuild_provider.transition_type = 5 #-SINE-#
log(f"After transition_type change: lut_dirty = {rebuild_provider._lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new transition: lut_dirty = {rebuild_provider._lut_dirty}")

View File

@ -82,7 +82,7 @@ provider.brightness = 200
log(f"After brightness change: _lut_dirty = {provider._lut_dirty}")
provider._lut_dirty = false
provider.transition_type = animation.SINE
provider.transition_type = 5 #-SINE-#
log(f"After transition_type change: _lut_dirty = {provider._lut_dirty}")
provider._lut_dirty = false