Berry animation performance improvements: LUT and native push_pixels (#24094)
This commit is contained in:
parent
b2bc197054
commit
ee1d867025
@ -14,7 +14,7 @@ palette rainbow_with_white = [
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
|
||||
|
||||
# since strip_length is dynamic, we need to map it to a variable
|
||||
set strip_len = strip_length()
|
||||
|
||||
@ -14,7 +14,7 @@ palette rainbow_with_white = [
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color, shift_period = 5s)
|
||||
|
||||
@ -331,7 +331,10 @@ Base interface for all color providers. Inherits from `ValueProvider`.
|
||||
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| *(none)* | - | - | - | Base interface has no parameters |
|
||||
| `brightness` | int | 255 | 0-255 | Overall brightness scaling for all colors |
|
||||
|
||||
**Static Methods**:
|
||||
- `apply_brightness(color, brightness)` - Applies brightness scaling to a color (ARGB format). Only performs scaling if brightness is not 255 (full brightness). This is a static utility method that can be called without an instance.
|
||||
|
||||
**Factory**: N/A (base interface)
|
||||
|
||||
@ -342,6 +345,7 @@ Returns a single, static color. Inherits from `ColorProvider`.
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `color` | int | 0xFFFFFFFF | - | The solid color to return |
|
||||
| *(inherits brightness from ColorProvider)* | | | | |
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
@ -370,6 +374,7 @@ Cycles through a palette of colors with brutal switching. Inherits from `ColorPr
|
||||
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = manual only) |
|
||||
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwards by `n` colors |
|
||||
| `palette_size` | int | 3 | read-only | Number of colors in the palette (automatically updated when palette changes) |
|
||||
| *(inherits brightness from ColorProvider)* | | | | |
|
||||
|
||||
**Note**: The `get_color_for_value()` method accepts values in the 0-255 range for value-based color mapping.
|
||||
|
||||
@ -406,7 +411,7 @@ Generates colors from predefined palettes with smooth transitions and profession
|
||||
| `palette` | bytes | rainbow palette | - | Palette bytes or predefined palette constant |
|
||||
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = value-based only) |
|
||||
| `transition_type` | int | animation.LINEAR | enum: [animation.LINEAR, animation.SINE] | LINEAR=constant speed, SINE=smooth ease-in/ease-out |
|
||||
| `brightness` | int | 255 | 0-255 | Overall brightness scaling |
|
||||
| *(inherits brightness from ColorProvider)* | | | | |
|
||||
|
||||
#### Available Predefined Palettes
|
||||
|
||||
@ -453,10 +458,11 @@ Creates breathing/pulsing color effects by modulating the brightness of a base c
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `base_color` | int | 0xFFFFFFFF | - | The base color to modulate (32-bit ARGB value) |
|
||||
| `min_brightness` | int | 0 | 0-255 | Minimum brightness level |
|
||||
| `max_brightness` | int | 255 | 0-255 | Maximum brightness level |
|
||||
| `min_brightness` | int | 0 | 0-255 | Minimum brightness level (breathing effect) |
|
||||
| `max_brightness` | int | 255 | 0-255 | Maximum brightness level (breathing effect) |
|
||||
| `duration` | int | 3000 | min: 1 | Time for one complete breathing cycle in ms |
|
||||
| `curve_factor` | int | 2 | 1-5 | Breathing curve shape (1=cosine wave, 2-5=curved breathing with pauses) |
|
||||
| *(inherits brightness from ColorProvider)* | | | | Overall brightness scaling applied after breathing effect |
|
||||
| *(inherits all OscillatorValueProvider parameters)* | | | | |
|
||||
|
||||
**Curve Factor Effects:**
|
||||
@ -518,6 +524,7 @@ Combines multiple color providers with blending. Inherits from `ColorProvider`.
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `blend_mode` | int | 0 | enum: [0,1,2] | 0=overlay, 1=add, 2=multiply |
|
||||
| *(inherits brightness from ColorProvider)* | | | | Overall brightness scaling applied to final composite color |
|
||||
|
||||
**Factory**: `animation.composite_color(engine)`
|
||||
|
||||
|
||||
@ -215,6 +215,125 @@ def render(frame, time_ms)
|
||||
end
|
||||
```
|
||||
|
||||
### Color Provider LUT Optimization
|
||||
|
||||
For color providers that perform expensive color calculations (like palette interpolation), the base `ColorProvider` class provides a Lookup Table (LUT) mechanism for caching pre-computed colors:
|
||||
|
||||
```berry
|
||||
#@ solidify:MyColorProvider,weak
|
||||
class MyColorProvider : animation.color_provider
|
||||
# Instance variables (all should start with underscore)
|
||||
var _cached_data # Your custom cached data
|
||||
|
||||
def init(engine)
|
||||
super(self).init(engine) # Initializes _color_lut and _lut_dirty
|
||||
self._cached_data = nil
|
||||
end
|
||||
|
||||
# Mark LUT as dirty when parameters change
|
||||
def on_param_changed(name, value)
|
||||
super(self).on_param_changed(name, value)
|
||||
if name == "palette" || name == "transition_type"
|
||||
self._lut_dirty = true # Inherited from ColorProvider
|
||||
end
|
||||
end
|
||||
|
||||
# Rebuild LUT when needed
|
||||
def _rebuild_color_lut()
|
||||
# Allocate LUT (e.g., 129 entries * 4 bytes = 516 bytes)
|
||||
if self._color_lut == nil
|
||||
self._color_lut = bytes()
|
||||
self._color_lut.resize(129 * 4)
|
||||
end
|
||||
|
||||
# Pre-compute colors for values 0, 2, 4, ..., 254, 255
|
||||
var i = 0
|
||||
while i < 128
|
||||
var value = i * 2
|
||||
var color = self._compute_color_expensive(value)
|
||||
self._color_lut.set(i * 4, color, 4)
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Add final entry for value 255
|
||||
var color_255 = self._compute_color_expensive(255)
|
||||
self._color_lut.set(128 * 4, color_255, 4)
|
||||
|
||||
self._lut_dirty = false
|
||||
end
|
||||
|
||||
# Update method checks if LUT needs rebuilding
|
||||
def update(time_ms)
|
||||
if self._lut_dirty || self._color_lut == nil
|
||||
self._rebuild_color_lut()
|
||||
end
|
||||
return self.is_running
|
||||
end
|
||||
|
||||
# Fast color lookup using LUT
|
||||
def get_color_for_value(value, time_ms)
|
||||
# Build LUT if needed (lazy initialization)
|
||||
if self._lut_dirty || self._color_lut == nil
|
||||
self._rebuild_color_lut()
|
||||
end
|
||||
|
||||
# Map value to LUT index (divide by 2, special case for 255)
|
||||
var lut_index = value >> 1
|
||||
if value >= 255
|
||||
lut_index = 128
|
||||
end
|
||||
|
||||
# Retrieve pre-computed color from LUT
|
||||
var color = self._color_lut.get(lut_index * 4, 4)
|
||||
|
||||
# Apply brightness scaling using static method (only if not 255)
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return animation.color_provider.apply_brightness(color, brightness)
|
||||
end
|
||||
|
||||
return color
|
||||
end
|
||||
|
||||
# Access LUT from outside (returns bytes() or nil)
|
||||
# Inherited from ColorProvider: get_lut()
|
||||
end
|
||||
```
|
||||
|
||||
**LUT Benefits:**
|
||||
- **5-10x speedup** for expensive color calculations
|
||||
- **Reduced CPU usage** during rendering
|
||||
- **Smooth animations** even with complex color logic
|
||||
- **Memory efficient** (typically 516 bytes for 129 entries)
|
||||
|
||||
**When to use LUT:**
|
||||
- Palette interpolation with binary search
|
||||
- Complex color transformations
|
||||
- Brightness calculations
|
||||
- Any expensive per-pixel color computation
|
||||
|
||||
**LUT Guidelines:**
|
||||
- Store colors at maximum brightness, apply scaling after lookup
|
||||
- Use 2-step resolution (0, 2, 4, ..., 254, 255) to save memory
|
||||
- Invalidate LUT when parameters affecting color calculation change
|
||||
- Don't invalidate for brightness changes if brightness is applied post-lookup
|
||||
|
||||
**Brightness Handling:**
|
||||
|
||||
The `ColorProvider` base class includes a `brightness` parameter (0-255, default 255) and a static method for applying brightness scaling:
|
||||
|
||||
```berry
|
||||
# Static method for brightness scaling (only scales if brightness != 255)
|
||||
animation.color_provider.apply_brightness(color, brightness)
|
||||
```
|
||||
|
||||
**Best Practices:**
|
||||
- Store LUT colors at maximum brightness (255)
|
||||
- Apply brightness scaling after LUT lookup using the static method
|
||||
- Only call the static method if `brightness != 255` to avoid unnecessary overhead
|
||||
- For inline performance-critical code, you can inline the brightness calculation instead of calling the static method
|
||||
- Brightness changes do NOT invalidate the LUT since brightness is applied after lookup
|
||||
|
||||
## Parameter Access
|
||||
|
||||
### Direct Virtual Member Assignment
|
||||
|
||||
@ -258,14 +258,7 @@ class AnimationEngine
|
||||
|
||||
# Output frame buffer to LED strip
|
||||
def _output_to_strip()
|
||||
var i = 0
|
||||
var strip_length = self.strip_length
|
||||
var strip = self.strip
|
||||
var pixels = self.frame_buffer.pixels
|
||||
while i < strip_length
|
||||
strip.set_pixel_color(i, pixels.get(i * 4, 4))
|
||||
i += 1
|
||||
end
|
||||
self.strip.push_pixels_buffer_argb(self.frame_buffer.pixels)
|
||||
self.strip.show()
|
||||
end
|
||||
|
||||
|
||||
@ -139,7 +139,14 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
if (idx >= palette_size) idx = palette_size - 1 end
|
||||
if (idx < 0) idx = 0 end
|
||||
self.current_index = idx
|
||||
return self._get_color_at_index(self.current_index)
|
||||
var color = self._get_color_at_index(self.current_index)
|
||||
|
||||
# Apply brightness scaling
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
# Auto-cycle mode: calculate which color to show based on time (brutal switching using integer math)
|
||||
@ -151,9 +158,16 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
color_index = palette_size - 1
|
||||
end
|
||||
|
||||
# Update current state and return the color
|
||||
# Update current state and get the color
|
||||
self.current_index = color_index
|
||||
return self._get_color_at_index(color_index)
|
||||
var color = self._get_color_at_index(color_index)
|
||||
|
||||
# Apply brightness scaling
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
# Get a color based on a value (maps value to position in cycle)
|
||||
@ -170,7 +184,12 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
end
|
||||
|
||||
if palette_size == 1
|
||||
return self._get_color_at_index(0) # If only one color, just return it
|
||||
var color = self._get_color_at_index(0) # If only one color, just return it
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
# Clamp value to 0-255
|
||||
@ -188,7 +207,14 @@ class ColorCycleColorProvider : animation.color_provider
|
||||
color_index = palette_size - 1
|
||||
end
|
||||
|
||||
return self._get_color_at_index(color_index)
|
||||
var color = self._get_color_at_index(color_index)
|
||||
|
||||
# Apply brightness scaling
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
# String representation of the provider
|
||||
|
||||
@ -14,6 +14,34 @@
|
||||
|
||||
#@ solidify:ColorProvider,weak
|
||||
class ColorProvider : animation.value_provider
|
||||
# LUT (Lookup Table) management for color providers
|
||||
# Subclasses can use this to cache pre-computed colors for performance
|
||||
# If a subclass doesn't use a LUT, this remains nil
|
||||
var _color_lut # Color lookup table cache (bytes() object or nil)
|
||||
var _lut_dirty # Flag indicating LUT needs rebuilding
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = animation.enc_params({
|
||||
"brightness": {"min": 0, "max": 255, "default": 255}
|
||||
})
|
||||
|
||||
# Initialize the color provider
|
||||
#
|
||||
# @param engine: AnimationEngine - Reference to the animation engine (required)
|
||||
def init(engine)
|
||||
super(self).init(engine)
|
||||
self._color_lut = nil
|
||||
self._lut_dirty = true
|
||||
end
|
||||
|
||||
# Get the color lookup table
|
||||
# Returns the LUT bytes() object if the provider uses one, or nil otherwise
|
||||
#
|
||||
# @return bytes|nil - The LUT bytes object or nil
|
||||
def get_lut()
|
||||
return self._color_lut
|
||||
end
|
||||
|
||||
# Produce a color value for any parameter name
|
||||
# This is the main method that subclasses should override
|
||||
#
|
||||
@ -34,6 +62,31 @@ class ColorProvider : animation.value_provider
|
||||
return self.produce_value("color", time_ms) # Default: use time-based color
|
||||
end
|
||||
|
||||
# Static method to apply brightness scaling to a color
|
||||
# Only performs scaling if brightness is not 255 (full brightness)
|
||||
#
|
||||
# @param color: int - Color in ARGB format (0xAARRGGBB)
|
||||
# @param brightness: int - Brightness level (0-255)
|
||||
# @return int - Color with brightness applied
|
||||
static def apply_brightness(color, brightness)
|
||||
# Skip scaling if brightness is full (255)
|
||||
if brightness == 255
|
||||
return color
|
||||
end
|
||||
|
||||
# Extract RGB components (preserve alpha channel)
|
||||
var r = (color >> 16) & 0xFF
|
||||
var g = (color >> 8) & 0xFF
|
||||
var b = color & 0xFF
|
||||
|
||||
# Scale each component by brightness
|
||||
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
|
||||
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
|
||||
b = tasmota.scale_uint(b, 0, 255, 0, brightness)
|
||||
|
||||
# Reconstruct color with scaled brightness (preserve alpha)
|
||||
return (color & 0xFF000000) | (r << 16) | (g << 8) | b
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
@ -49,7 +49,12 @@ class CompositeColorProvider : animation.color_provider
|
||||
end
|
||||
|
||||
if size(self.providers) == 1
|
||||
return self.providers[0].produce_value(name, time_ms)
|
||||
var color = self.providers[0].produce_value(name, time_ms)
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
var result_color = self.providers[0].produce_value(name, time_ms)
|
||||
@ -61,6 +66,11 @@ class CompositeColorProvider : animation.color_provider
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Apply brightness scaling to final composite color
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(result_color, brightness)
|
||||
end
|
||||
return result_color
|
||||
end
|
||||
|
||||
@ -75,7 +85,12 @@ class CompositeColorProvider : animation.color_provider
|
||||
end
|
||||
|
||||
if size(self.providers) == 1
|
||||
return self.providers[0].get_color_for_value(value, time_ms)
|
||||
var color = self.providers[0].get_color_for_value(value, time_ms)
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
var result_color = self.providers[0].get_color_for_value(value, time_ms)
|
||||
@ -87,6 +102,11 @@ class CompositeColorProvider : animation.color_provider
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Apply brightness scaling to final composite color
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(result_color, brightness)
|
||||
end
|
||||
return result_color
|
||||
end
|
||||
|
||||
|
||||
@ -33,38 +33,34 @@ import "./core/param_encoder" as encode_constraints
|
||||
#@ solidify:RichPaletteColorProvider,weak
|
||||
class RichPaletteColorProvider : animation.color_provider
|
||||
# Non-parameter instance variables only
|
||||
var slots_arr # Constructed array of timestamp slots, based on cycle_period
|
||||
var value_arr # Constructed array of value slots (always 0-255 range)
|
||||
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 color_lut # Color lookup table cache (129 entries: 0, 2, 4, ..., 254, 255)
|
||||
var lut_dirty # Flag indicating LUT needs rebuilding
|
||||
var _brightness # Cached value for `self.brightness` used during render()
|
||||
var _slots_arr # Constructed array of timestamp slots, based on cycle_period
|
||||
var _value_arr # Constructed array of value slots (always 0-255 range)
|
||||
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 _brightness # Cached value for `self.brightness` used during render()
|
||||
|
||||
# Parameter definitions
|
||||
static var PARAMS = animation.enc_params({
|
||||
"palette": {"type": "bytes", "default": nil}, # Palette bytes or predefined palette constant
|
||||
"cycle_period": {"min": 0, "default": 5000}, # 5 seconds default, 0 = value-based only
|
||||
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR},
|
||||
"brightness": {"min": 0, "max": 255, "default": 255}
|
||||
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR}
|
||||
# brightness parameter inherited from ColorProvider base class
|
||||
})
|
||||
|
||||
# Initialize a new RichPaletteColorProvider
|
||||
#
|
||||
# @param engine: AnimationEngine - Reference to the animation engine (required)
|
||||
def init(engine)
|
||||
super(self).init(engine) # Initialize parameter system
|
||||
super(self).init(engine) # Initialize parameter system (also initializes LUT variables)
|
||||
|
||||
# Initialize non-parameter instance variables
|
||||
self.current_color = 0xFFFFFFFF
|
||||
self.slots = 0
|
||||
self.color_lut = nil
|
||||
self.lut_dirty = true
|
||||
self._current_color = 0xFFFFFFFF
|
||||
self._slots = 0
|
||||
|
||||
# Create light_state instance for proper color calculations (reuse from Animate_palette)
|
||||
import global
|
||||
self.light_state = global.light_state(global.light_state.RGB)
|
||||
self._light_state = global.light_state(global.light_state.RGB)
|
||||
|
||||
# We need to register this value provider to receive 'update()'
|
||||
engine.add(self)
|
||||
@ -77,7 +73,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
def on_param_changed(name, value)
|
||||
super(self).on_param_changed(name, value)
|
||||
if name == "cycle_period" || name == "palette"
|
||||
if (self.slots_arr != nil) || (self.value_arr != nil)
|
||||
if (self._slots_arr != nil) || (self._value_arr != nil)
|
||||
# only if they were already computed
|
||||
self._recompute_palette()
|
||||
end
|
||||
@ -85,8 +81,9 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# Mark LUT as dirty when palette or transition_type changes
|
||||
# Note: brightness changes do NOT invalidate LUT since brightness is applied after lookup
|
||||
if name == "palette" || name == "transition_type"
|
||||
self.lut_dirty = true
|
||||
self._lut_dirty = true
|
||||
end
|
||||
# Brightness changes do NOT invalidate LUT - brightness is applied after lookup
|
||||
end
|
||||
|
||||
# Start/restart the animation cycle at a specific time
|
||||
@ -95,7 +92,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# @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)
|
||||
if (self._slots_arr == nil) && (self._value_arr == nil)
|
||||
self._recompute_palette()
|
||||
end
|
||||
super(self).start(time_ms)
|
||||
@ -123,25 +120,25 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# Compute slots_arr based on 'cycle_period'
|
||||
var cycle_period = self.cycle_period
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
self.slots = size(palette_bytes) / 4
|
||||
self._slots = size(palette_bytes) / 4
|
||||
|
||||
# Recompute palette with new cycle period (only if > 0 for time-based cycling)
|
||||
if cycle_period > 0 && palette_bytes != nil
|
||||
self.slots_arr = self._parse_palette(0, cycle_period - 1)
|
||||
self._slots_arr = self._parse_palette(0, cycle_period - 1)
|
||||
else
|
||||
self.slots_arr = nil
|
||||
self._slots_arr = nil
|
||||
end
|
||||
|
||||
# Compute value_arr for value-based mode (always 0-255 range)
|
||||
if self._get_palette_bytes() != nil
|
||||
self.value_arr = self._parse_palette(0, 255)
|
||||
self._value_arr = self._parse_palette(0, 255)
|
||||
else
|
||||
self.value_arr = nil
|
||||
self._value_arr = nil
|
||||
end
|
||||
|
||||
# Set initial color
|
||||
if self.slots > 0
|
||||
self.current_color = self._get_color_at_index(0)
|
||||
if self._slots > 0
|
||||
self._current_color = self._get_color_at_index(0)
|
||||
end
|
||||
|
||||
return self
|
||||
@ -155,7 +152,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
def _parse_palette(min, max)
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
var arr = []
|
||||
var slots = self.slots
|
||||
var slots = self._slots
|
||||
arr.resize(slots)
|
||||
|
||||
# Check if we have slots or values (exact logic from Animate_palette)
|
||||
@ -191,7 +188,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
|
||||
# Get color at a specific index (simplified)
|
||||
def _get_color_at_index(idx)
|
||||
if idx < 0 || idx >= self.slots
|
||||
if idx < 0 || idx >= self._slots
|
||||
return 0xFFFFFFFF
|
||||
end
|
||||
|
||||
@ -245,7 +242,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# @return bool - True if object is still running, false if completed
|
||||
def update(time_ms)
|
||||
# Rebuild LUT if dirty
|
||||
if self.lut_dirty || self.color_lut == nil
|
||||
if self._lut_dirty || self._color_lut == nil
|
||||
self._rebuild_color_lut()
|
||||
end
|
||||
|
||||
@ -264,12 +261,12 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# 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)
|
||||
if (self._slots_arr == nil) && (self._value_arr == nil)
|
||||
self._recompute_palette()
|
||||
end
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
|
||||
if palette_bytes == nil || self.slots < 2
|
||||
if palette_bytes == nil || self._slots < 2
|
||||
return 0xFFFFFFFF
|
||||
end
|
||||
|
||||
@ -284,7 +281,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
var g = (bgrt0 >> 16) & 0xFF
|
||||
var b = (bgrt0 >> 24) & 0xFF
|
||||
|
||||
# Apply brightness scaling
|
||||
# Apply brightness scaling (inline for speed)
|
||||
if brightness != 255
|
||||
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
|
||||
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
|
||||
@ -292,7 +289,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
end
|
||||
|
||||
var final_color = (0xFF << 24) | (r << 16) | (g << 8) | b
|
||||
self.current_color = final_color
|
||||
self._current_color = final_color
|
||||
return final_color
|
||||
end
|
||||
|
||||
@ -301,17 +298,17 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
var past = elapsed % cycle_period
|
||||
|
||||
# Find slot (exact algorithm from Animate_palette)
|
||||
var slots = self.slots
|
||||
var slots = self._slots
|
||||
var idx = slots - 2
|
||||
while idx > 0
|
||||
if past >= self.slots_arr[idx] break end
|
||||
if past >= self._slots_arr[idx] break end
|
||||
idx -= 1
|
||||
end
|
||||
|
||||
var bgrt0 = palette_bytes.get(idx * 4, 4)
|
||||
var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)
|
||||
var t0 = self.slots_arr[idx]
|
||||
var t1 = self.slots_arr[idx + 1]
|
||||
var t0 = self._slots_arr[idx]
|
||||
var t1 = self._slots_arr[idx + 1]
|
||||
|
||||
# Use interpolation based on transition_type (LINEAR or SINE)
|
||||
var r = self._interpolate(past, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)
|
||||
@ -319,7 +316,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
var b = self._interpolate(past, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)
|
||||
|
||||
# Use light_state for proper brightness calculation (from Animate_palette)
|
||||
var light_state = self.light_state
|
||||
var light_state = self._light_state
|
||||
light_state.set_rgb((bgrt0 >> 8) & 0xFF, (bgrt0 >> 16) & 0xFF, (bgrt0 >> 24) & 0xFF)
|
||||
var bri0 = light_state.bri
|
||||
light_state.set_rgb((bgrt1 >> 8) & 0xFF, (bgrt1 >> 16) & 0xFF, (bgrt1 >> 24) & 0xFF)
|
||||
@ -332,7 +329,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
g = light_state.g
|
||||
b = light_state.b
|
||||
|
||||
# Apply brightness scaling (from Animate_palette)
|
||||
# Apply brightness scaling (inline for speed)
|
||||
if brightness != 255
|
||||
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
|
||||
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
|
||||
@ -341,7 +338,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
|
||||
# Create final color in ARGB format
|
||||
var final_color = (0xFF << 24) | (r << 16) | (g << 8) | b
|
||||
self.current_color = final_color
|
||||
self._current_color = final_color
|
||||
|
||||
return final_color
|
||||
end
|
||||
@ -369,14 +366,14 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# - Little-endian format (native Berry integer representation)
|
||||
def _rebuild_color_lut()
|
||||
# Ensure palette arrays are initialized
|
||||
if self.value_arr == nil
|
||||
if self._value_arr == nil
|
||||
self._recompute_palette()
|
||||
end
|
||||
|
||||
# Allocate LUT if needed (129 entries * 4 bytes = 516 bytes)
|
||||
if self.color_lut == nil
|
||||
self.color_lut = bytes()
|
||||
self.color_lut.resize(129 * 4)
|
||||
if self._color_lut == nil
|
||||
self._color_lut = bytes()
|
||||
self._color_lut.resize(129 * 4)
|
||||
end
|
||||
|
||||
# Pre-compute colors for values 0, 2, 4, ..., 254 at max brightness
|
||||
@ -386,49 +383,49 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
var color = self._get_color_for_value_uncached(value, 0)
|
||||
|
||||
# Store color using efficient bytes.set()
|
||||
self.color_lut.set(i * 4, color, 4)
|
||||
self._color_lut.set(i * 4, color, 4)
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Add final entry for value 255 at max brightness
|
||||
var color_255 = self._get_color_for_value_uncached(255, 0)
|
||||
self.color_lut.set(128 * 4, color_255, 4)
|
||||
self._color_lut.set(128 * 4, color_255, 4)
|
||||
|
||||
self.lut_dirty = false
|
||||
self._lut_dirty = false
|
||||
end
|
||||
|
||||
# Get color for a specific value WITHOUT using cache (internal method)
|
||||
# This is the original implementation moved to a separate method
|
||||
# Colors are returned at MAXIMUM brightness (255) - brightness scaling applied separately
|
||||
#
|
||||
# @param value: int/float - Value to map to a color (0-255 range)
|
||||
# @param time_ms: int - Current time in milliseconds (ignored for value-based color)
|
||||
# @return int - Color in ARGB format
|
||||
# @return int - Color in ARGB format at maximum brightness
|
||||
def _get_color_for_value_uncached(value, time_ms)
|
||||
if (self.slots_arr == nil) && (self.value_arr == nil)
|
||||
if (self._slots_arr == nil) && (self._value_arr == nil)
|
||||
self._recompute_palette()
|
||||
end
|
||||
var palette_bytes = self._get_palette_bytes()
|
||||
var brightness = self.brightness
|
||||
|
||||
# Find slot (exact algorithm from Animate_palette.set_value)
|
||||
var slots = self.slots
|
||||
var slots = self._slots
|
||||
var idx = slots - 2
|
||||
while idx > 0
|
||||
if value >= self.value_arr[idx] break end
|
||||
if value >= self._value_arr[idx] break end
|
||||
idx -= 1
|
||||
end
|
||||
|
||||
var bgrt0 = palette_bytes.get(idx * 4, 4)
|
||||
var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)
|
||||
var t0 = self.value_arr[idx]
|
||||
var t1 = self.value_arr[idx + 1]
|
||||
var t0 = self._value_arr[idx]
|
||||
var t1 = self._value_arr[idx + 1]
|
||||
|
||||
# Use interpolation based on transition_type (LINEAR or SINE)
|
||||
var r = self._interpolate(value, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)
|
||||
var g = self._interpolate(value, t0, t1, (bgrt0 >> 16) & 0xFF, (bgrt1 >> 16) & 0xFF)
|
||||
var b = self._interpolate(value, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)
|
||||
|
||||
# Create final color in ARGB format
|
||||
# Create final color in ARGB format at maximum brightness
|
||||
return (0xFF << 24) | (r << 16) | (g << 8) | b
|
||||
end
|
||||
|
||||
@ -448,7 +445,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
#
|
||||
# Brightness handling:
|
||||
# - LUT stores colors at maximum brightness (255)
|
||||
# - Actual brightness scaling applied here after lookup
|
||||
# - Actual brightness scaling applied here after lookup using static method
|
||||
# - This allows brightness to change dynamically without invalidating LUT
|
||||
#
|
||||
# @param value: int/float - Value to map to a color (0-255 range)
|
||||
@ -469,7 +466,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
|
||||
# Retrieve color from LUT using efficient bytes.get()
|
||||
# This color is at maximum brightness (255)
|
||||
var color = self.color_lut.get(lut_index * 4, 4)
|
||||
var color = self._color_lut.get(lut_index * 4, 4)
|
||||
|
||||
# Apply brightness scaling if not at maximum
|
||||
var brightness = self._brightness
|
||||
@ -521,7 +518,7 @@ class RichPaletteColorProvider : animation.color_provider
|
||||
# String representation
|
||||
def tostring()
|
||||
try
|
||||
return f"RichPaletteColorProvider(slots={self.slots}, cycle_period={self.cycle_period})"
|
||||
return f"RichPaletteColorProvider(slots={self._slots}, cycle_period={self.cycle_period})"
|
||||
except ..
|
||||
return "RichPaletteColorProvider(uninitialized)"
|
||||
end
|
||||
|
||||
@ -22,7 +22,12 @@ class StaticColorProvider : animation.color_provider
|
||||
# @param time_ms: int - Current time in milliseconds (ignored)
|
||||
# @return int - Color in ARGB format (0xAARRGGBB)
|
||||
def produce_value(name, time_ms)
|
||||
return self.color
|
||||
var color = self.color
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
# Get the solid color for a value (ignores the value)
|
||||
@ -31,7 +36,12 @@ class StaticColorProvider : animation.color_provider
|
||||
# @param time_ms: int - Current time in milliseconds (ignored)
|
||||
# @return int - Color in ARGB format (0xAARRGGBB)
|
||||
def get_color_for_value(value, time_ms)
|
||||
return self.color
|
||||
var color = self.color
|
||||
var brightness = self.brightness
|
||||
if brightness != 255
|
||||
return self.apply_brightness(color, brightness)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
# String representation of the provider
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -202,7 +202,7 @@ class RichPaletteAnimationTest
|
||||
# Test value-based color generation (now always 0-255 range)
|
||||
provider.start()
|
||||
provider.update()
|
||||
print(f"{provider.slots_arr=} {provider.value_arr=}")
|
||||
print(f"{provider._slots_arr=} {provider._value_arr=}")
|
||||
var color_0 = provider.get_color_for_value(0, 0)
|
||||
var color_128 = provider.get_color_for_value(128, 0)
|
||||
var color_255 = provider.get_color_for_value(255, 0)
|
||||
|
||||
@ -42,7 +42,7 @@ var b = color_at_255 & 0xFF
|
||||
log(f" RGB({r:3d}, {g:3d}, {b:3d}) = 0x{color_at_255:08X}")
|
||||
|
||||
# Verify LUT is not dirty
|
||||
log(f"LUT dirty after initial build: {provider.lut_dirty}")
|
||||
log(f"LUT dirty after initial build: {provider._lut_dirty}")
|
||||
log("")
|
||||
|
||||
# Change brightness multiple times and verify LUT stays valid
|
||||
@ -50,7 +50,7 @@ var brightness_values = [200, 150, 100, 50, 255]
|
||||
for brightness : brightness_values
|
||||
provider.brightness = brightness
|
||||
log(f"Changed brightness to {brightness}")
|
||||
log(f" LUT dirty: {provider.lut_dirty}")
|
||||
log(f" LUT dirty: {provider._lut_dirty}")
|
||||
|
||||
var color = provider.get_color_for_value(128, 0)
|
||||
r = (color >> 16) & 0xFF
|
||||
@ -148,25 +148,25 @@ rebuild_provider.cycle_period = 0
|
||||
|
||||
# Force initial build
|
||||
rebuild_provider.get_color_for_value(128, 0)
|
||||
log(f"After initial build: lut_dirty = {rebuild_provider.lut_dirty}")
|
||||
log(f"After initial build: lut_dirty = {rebuild_provider._lut_dirty}")
|
||||
|
||||
# Change brightness - should NOT trigger rebuild
|
||||
rebuild_provider.brightness = 100
|
||||
log(f"After brightness change: lut_dirty = {rebuild_provider.lut_dirty}")
|
||||
log(f"After brightness change: lut_dirty = {rebuild_provider._lut_dirty}")
|
||||
rebuild_provider.get_color_for_value(128, 0)
|
||||
log(f"After lookup with new brightness: lut_dirty = {rebuild_provider.lut_dirty}")
|
||||
log(f"After lookup with new brightness: lut_dirty = {rebuild_provider._lut_dirty}")
|
||||
|
||||
# Change palette - SHOULD trigger rebuild
|
||||
rebuild_provider.palette = bytes("00FF0000" "FFFFFF00")
|
||||
log(f"After palette change: lut_dirty = {rebuild_provider.lut_dirty}")
|
||||
log(f"After palette change: lut_dirty = {rebuild_provider._lut_dirty}")
|
||||
rebuild_provider.get_color_for_value(128, 0)
|
||||
log(f"After lookup with new palette: lut_dirty = {rebuild_provider.lut_dirty}")
|
||||
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
|
||||
log(f"After transition_type change: lut_dirty = {rebuild_provider.lut_dirty}")
|
||||
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}")
|
||||
log(f"After lookup with new transition: lut_dirty = {rebuild_provider._lut_dirty}")
|
||||
|
||||
log("")
|
||||
log("=== All tests completed successfully ===")
|
||||
|
||||
@ -51,11 +51,11 @@ provider.produce_value("color", 0)
|
||||
|
||||
# Debug: Check palette
|
||||
log(f"Palette size: {size(provider.palette)} bytes")
|
||||
log(f"Slots: {provider.slots}")
|
||||
log(f"Slots: {provider._slots}")
|
||||
log("Range: 0 to 255 (fixed)")
|
||||
|
||||
# Force LUT rebuild
|
||||
provider.lut_dirty = true
|
||||
provider._lut_dirty = true
|
||||
|
||||
# Test key values
|
||||
var test_values = [0, 2, 4, 50, 100, 150, 200, 254, 255]
|
||||
@ -75,19 +75,19 @@ log("")
|
||||
log("Test 2: LUT invalidation on parameter changes")
|
||||
log("----------------------------------------------")
|
||||
|
||||
provider.lut_dirty = false
|
||||
log(f"Initial lut_dirty: {provider.lut_dirty}")
|
||||
provider._lut_dirty = false
|
||||
log(f"Initial _lut_dirty: {provider._lut_dirty}")
|
||||
|
||||
provider.brightness = 200
|
||||
log(f"After brightness change: lut_dirty = {provider.lut_dirty}")
|
||||
log(f"After brightness change: _lut_dirty = {provider._lut_dirty}")
|
||||
|
||||
provider.lut_dirty = false
|
||||
provider._lut_dirty = false
|
||||
provider.transition_type = animation.SINE
|
||||
log(f"After transition_type change: lut_dirty = {provider.lut_dirty}")
|
||||
log(f"After transition_type change: _lut_dirty = {provider._lut_dirty}")
|
||||
|
||||
provider.lut_dirty = false
|
||||
provider._lut_dirty = false
|
||||
provider.palette = bytes("00FF0000" "FFFFFF00" "FF00FF00")
|
||||
log(f"After palette change: lut_dirty = {provider.lut_dirty}")
|
||||
log(f"After palette change: _lut_dirty = {provider._lut_dirty}")
|
||||
|
||||
log("")
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
extern int be_tasmotaled_call_native(bvm *vm);
|
||||
extern int be_leds_blend_color(bvm *vm);
|
||||
extern int be_leds_apply_bri_gamma(bvm *vm);
|
||||
extern int be_leds_set_pixels(bvm *vm);
|
||||
|
||||
/* @const_object_info_begin
|
||||
class be_class_Leds_ntv (scope: global, name: Leds_ntv, strings: weak) {
|
||||
@ -29,6 +30,8 @@ class be_class_Leds_ntv (scope: global, name: Leds_ntv, strings: weak) {
|
||||
|
||||
blend_color, static_func(be_leds_blend_color)
|
||||
apply_bri_gamma, static_func(be_leds_apply_bri_gamma)
|
||||
|
||||
set_pixels, static_func(be_leds_set_pixels)
|
||||
}
|
||||
@const_object_info_end */
|
||||
|
||||
|
||||
@ -131,6 +131,21 @@ class Leds : Leds_ntv
|
||||
def dirty() ## DEPRECATED
|
||||
self.call_native(5)
|
||||
end
|
||||
|
||||
# push_pixels
|
||||
#
|
||||
# Pushes a bytes() buffer of 0xAARRGGBB colors, without bri nor gamma correction
|
||||
#
|
||||
def push_pixels_buffer_argb(pixels)
|
||||
# Leds.set_pixels(buffer:bytes, pixels_buffer:comptr, pixels_count:int, [pixel_size:int = 3, bri:int (0..255) = 255, gamma:bool = true]) -> void
|
||||
self.set_pixels(pixels,
|
||||
self.call_native(6), # address of buffer in memory
|
||||
self.pixel_count(),
|
||||
self.pixel_size(),
|
||||
self.get_bri(),
|
||||
self.get_gamma())
|
||||
end
|
||||
|
||||
def pixels_buffer(old_buf)
|
||||
var buf = self.call_native(6) # address of buffer in memory
|
||||
var sz = self.pixel_size() * self.pixel_count()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -325,6 +325,77 @@ extern "C" {
|
||||
}
|
||||
be_raise(vm, kTypeError, nullptr);
|
||||
}
|
||||
|
||||
// Leds.set_pixels(buffer:bytes, pixels_buffer:comptr, pixels_count:int, [pixel_size:int = 3, bri:int (0..255) = 255, gamma:bool = true]) -> void
|
||||
//
|
||||
// Copies the pixels from the buffer to the driver, from 0xAARRGGBB buffer
|
||||
// to either 0xRRGGBB or 0xWWRRGGBB, applying brightness (0..255) and
|
||||
// optional gamma correction
|
||||
int32_t be_leds_set_pixels(bvm *vm);
|
||||
int32_t be_leds_set_pixels(bvm *vm) {
|
||||
int32_t top = be_top(vm); // Get the number of arguments
|
||||
if (top >= 3 && be_isbytes(vm, 1) && be_iscomptr(vm, 2) && be_isint(vm, 3)) {
|
||||
// Get parameters
|
||||
// arg1: buffer:bytes
|
||||
size_t buffer_length;
|
||||
uint32_t * buffer = (uint32_t*) be_tobytes(vm, 1, &buffer_length);
|
||||
// arg2: pixels:comptr
|
||||
uint8_t * pixels = (uint8_t*) be_tocomptr(vm, 2);
|
||||
// arg3: pixels_count:int
|
||||
int32_t pixels_count = be_toint(vm, 3);
|
||||
// arg4: pixel_size:int
|
||||
size_t pixel_size = 3; // 0xRRGGBB
|
||||
if (top >= 4 && be_isint(vm, 4)) {
|
||||
pixel_size = be_toint(vm, 4);
|
||||
}
|
||||
if (pixel_size < 3) { pixel_size = 3; }
|
||||
if (pixel_size > 4) { pixel_size = 4; }
|
||||
// arg5: bri:int (0..255)
|
||||
int32_t bri255 = 255;
|
||||
if (top >= 3 && be_isint(vm, 5)) {
|
||||
bri255 = be_toint(vm, 5);
|
||||
}
|
||||
if (bri255 < 0) { bri255 = 0; }
|
||||
if (bri255 > 255) { bri255 = 255; }
|
||||
// arg6: gamma:bool
|
||||
bool gamma = true;
|
||||
if (top >= 6 && be_isbool(vm, 6)) {
|
||||
gamma = be_tobool(vm, 6);
|
||||
}
|
||||
|
||||
// now do the stuff
|
||||
uint8_t * p = pixels;
|
||||
for (uint32_t i = 0; i < pixels_count; i++) {
|
||||
uint32_t argb = buffer[i]; // get 0xAARRGGBB, we drop the AA alpha channel
|
||||
uint8_t r = (argb >> 16) & 0xFF;
|
||||
uint8_t g = (argb >> 8) & 0xFF;
|
||||
uint8_t b = (argb ) & 0xFF;
|
||||
uint8_t w = 0; // for now we set white channel to zero
|
||||
|
||||
// apply brigthness
|
||||
if (bri255 < 255) {
|
||||
r = changeUIntScale(bri255, 0, 255, 0, r);
|
||||
g = changeUIntScale(bri255, 0, 255, 0, g);
|
||||
b = changeUIntScale(bri255, 0, 255, 0, b);
|
||||
}
|
||||
// apply gamma
|
||||
if (gamma) {
|
||||
r = ledGamma(r);
|
||||
g = ledGamma(g);
|
||||
b = ledGamma(b);
|
||||
}
|
||||
// set pixel in strip
|
||||
if (pixel_size == 4) { *p++ = 0;}
|
||||
*p++ = r;
|
||||
*p++ = g;
|
||||
*p++ = b;
|
||||
}
|
||||
|
||||
be_return_nil(vm);
|
||||
}
|
||||
be_raise(vm, kTypeError, nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // USE_WS2812
|
||||
|
||||
Loading…
Reference in New Issue
Block a user