Skip to content

Commit ee1d867

Browse files
authored
Berry animation performance improvements: LUT and native push_pixels (#24094)
1 parent b2bc197 commit ee1d867

20 files changed

+2361
-1774
lines changed

lib/libesp32/berry_animation/anim_examples/old/ocean_waves.anim renamed to lib/libesp32/berry_animation/anim_examples/ocean_waves.anim

File renamed without changes.

lib/libesp32/berry_animation/anim_examples/old/plasma_wave.anim renamed to lib/libesp32/berry_animation/anim_examples/plasma_wave.anim

File renamed without changes.

lib/libesp32/berry_animation/anim_tutorials/chap_1_32_color_pattern_spatial_osc.anim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ palette rainbow_with_white = [
1414
]
1515

1616
# define a color attribute that cycles over time, cycle is 10 seconds
17-
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
17+
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
1818

1919
# since strip_length is dynamic, we need to map it to a variable
2020
set strip_len = strip_length()

lib/libesp32/berry_animation/anim_tutorials/chap_1_33_color_pattern_spatial_rotate.anim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ palette rainbow_with_white = [
1414
]
1515

1616
# define a color attribute that cycles over time, cycle is 10 seconds
17-
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
17+
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
1818

1919
# define a gradient across the whole strip
2020
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color, shift_period = 5s)

lib/libesp32/berry_animation/docs/ANIMATION_CLASS_HIERARCHY.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,10 @@ Base interface for all color providers. Inherits from `ValueProvider`.
331331

332332
| Parameter | Type | Default | Constraints | Description |
333333
|-----------|------|---------|-------------|-------------|
334-
| *(none)* | - | - | - | Base interface has no parameters |
334+
| `brightness` | int | 255 | 0-255 | Overall brightness scaling for all colors |
335+
336+
**Static Methods**:
337+
- `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.
335338

336339
**Factory**: N/A (base interface)
337340

@@ -342,6 +345,7 @@ Returns a single, static color. Inherits from `ColorProvider`.
342345
| Parameter | Type | Default | Constraints | Description |
343346
|-----------|------|---------|-------------|-------------|
344347
| `color` | int | 0xFFFFFFFF | - | The solid color to return |
348+
| *(inherits brightness from ColorProvider)* | | | | |
345349

346350
#### Usage Examples
347351

@@ -370,6 +374,7 @@ Cycles through a palette of colors with brutal switching. Inherits from `ColorPr
370374
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = manual only) |
371375
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwards by `n` colors |
372376
| `palette_size` | int | 3 | read-only | Number of colors in the palette (automatically updated when palette changes) |
377+
| *(inherits brightness from ColorProvider)* | | | | |
373378

374379
**Note**: The `get_color_for_value()` method accepts values in the 0-255 range for value-based color mapping.
375380

@@ -406,7 +411,7 @@ Generates colors from predefined palettes with smooth transitions and profession
406411
| `palette` | bytes | rainbow palette | - | Palette bytes or predefined palette constant |
407412
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = value-based only) |
408413
| `transition_type` | int | animation.LINEAR | enum: [animation.LINEAR, animation.SINE] | LINEAR=constant speed, SINE=smooth ease-in/ease-out |
409-
| `brightness` | int | 255 | 0-255 | Overall brightness scaling |
414+
| *(inherits brightness from ColorProvider)* | | | | |
410415

411416
#### Available Predefined Palettes
412417

@@ -453,10 +458,11 @@ Creates breathing/pulsing color effects by modulating the brightness of a base c
453458
| Parameter | Type | Default | Constraints | Description |
454459
|-----------|------|---------|-------------|-------------|
455460
| `base_color` | int | 0xFFFFFFFF | - | The base color to modulate (32-bit ARGB value) |
456-
| `min_brightness` | int | 0 | 0-255 | Minimum brightness level |
457-
| `max_brightness` | int | 255 | 0-255 | Maximum brightness level |
461+
| `min_brightness` | int | 0 | 0-255 | Minimum brightness level (breathing effect) |
462+
| `max_brightness` | int | 255 | 0-255 | Maximum brightness level (breathing effect) |
458463
| `duration` | int | 3000 | min: 1 | Time for one complete breathing cycle in ms |
459464
| `curve_factor` | int | 2 | 1-5 | Breathing curve shape (1=cosine wave, 2-5=curved breathing with pauses) |
465+
| *(inherits brightness from ColorProvider)* | | | | Overall brightness scaling applied after breathing effect |
460466
| *(inherits all OscillatorValueProvider parameters)* | | | | |
461467

462468
**Curve Factor Effects:**
@@ -518,6 +524,7 @@ Combines multiple color providers with blending. Inherits from `ColorProvider`.
518524
| Parameter | Type | Default | Constraints | Description |
519525
|-----------|------|---------|-------------|-------------|
520526
| `blend_mode` | int | 0 | enum: [0,1,2] | 0=overlay, 1=add, 2=multiply |
527+
| *(inherits brightness from ColorProvider)* | | | | Overall brightness scaling applied to final composite color |
521528

522529
**Factory**: `animation.composite_color(engine)`
523530

lib/libesp32/berry_animation/docs/ANIMATION_DEVELOPMENT.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,125 @@ def render(frame, time_ms)
215215
end
216216
```
217217

218+
### Color Provider LUT Optimization
219+
220+
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:
221+
222+
```berry
223+
#@ solidify:MyColorProvider,weak
224+
class MyColorProvider : animation.color_provider
225+
# Instance variables (all should start with underscore)
226+
var _cached_data # Your custom cached data
227+
228+
def init(engine)
229+
super(self).init(engine) # Initializes _color_lut and _lut_dirty
230+
self._cached_data = nil
231+
end
232+
233+
# Mark LUT as dirty when parameters change
234+
def on_param_changed(name, value)
235+
super(self).on_param_changed(name, value)
236+
if name == "palette" || name == "transition_type"
237+
self._lut_dirty = true # Inherited from ColorProvider
238+
end
239+
end
240+
241+
# Rebuild LUT when needed
242+
def _rebuild_color_lut()
243+
# Allocate LUT (e.g., 129 entries * 4 bytes = 516 bytes)
244+
if self._color_lut == nil
245+
self._color_lut = bytes()
246+
self._color_lut.resize(129 * 4)
247+
end
248+
249+
# Pre-compute colors for values 0, 2, 4, ..., 254, 255
250+
var i = 0
251+
while i < 128
252+
var value = i * 2
253+
var color = self._compute_color_expensive(value)
254+
self._color_lut.set(i * 4, color, 4)
255+
i += 1
256+
end
257+
258+
# Add final entry for value 255
259+
var color_255 = self._compute_color_expensive(255)
260+
self._color_lut.set(128 * 4, color_255, 4)
261+
262+
self._lut_dirty = false
263+
end
264+
265+
# Update method checks if LUT needs rebuilding
266+
def update(time_ms)
267+
if self._lut_dirty || self._color_lut == nil
268+
self._rebuild_color_lut()
269+
end
270+
return self.is_running
271+
end
272+
273+
# Fast color lookup using LUT
274+
def get_color_for_value(value, time_ms)
275+
# Build LUT if needed (lazy initialization)
276+
if self._lut_dirty || self._color_lut == nil
277+
self._rebuild_color_lut()
278+
end
279+
280+
# Map value to LUT index (divide by 2, special case for 255)
281+
var lut_index = value >> 1
282+
if value >= 255
283+
lut_index = 128
284+
end
285+
286+
# Retrieve pre-computed color from LUT
287+
var color = self._color_lut.get(lut_index * 4, 4)
288+
289+
# Apply brightness scaling using static method (only if not 255)
290+
var brightness = self.brightness
291+
if brightness != 255
292+
return animation.color_provider.apply_brightness(color, brightness)
293+
end
294+
295+
return color
296+
end
297+
298+
# Access LUT from outside (returns bytes() or nil)
299+
# Inherited from ColorProvider: get_lut()
300+
end
301+
```
302+
303+
**LUT Benefits:**
304+
- **5-10x speedup** for expensive color calculations
305+
- **Reduced CPU usage** during rendering
306+
- **Smooth animations** even with complex color logic
307+
- **Memory efficient** (typically 516 bytes for 129 entries)
308+
309+
**When to use LUT:**
310+
- Palette interpolation with binary search
311+
- Complex color transformations
312+
- Brightness calculations
313+
- Any expensive per-pixel color computation
314+
315+
**LUT Guidelines:**
316+
- Store colors at maximum brightness, apply scaling after lookup
317+
- Use 2-step resolution (0, 2, 4, ..., 254, 255) to save memory
318+
- Invalidate LUT when parameters affecting color calculation change
319+
- Don't invalidate for brightness changes if brightness is applied post-lookup
320+
321+
**Brightness Handling:**
322+
323+
The `ColorProvider` base class includes a `brightness` parameter (0-255, default 255) and a static method for applying brightness scaling:
324+
325+
```berry
326+
# Static method for brightness scaling (only scales if brightness != 255)
327+
animation.color_provider.apply_brightness(color, brightness)
328+
```
329+
330+
**Best Practices:**
331+
- Store LUT colors at maximum brightness (255)
332+
- Apply brightness scaling after LUT lookup using the static method
333+
- Only call the static method if `brightness != 255` to avoid unnecessary overhead
334+
- For inline performance-critical code, you can inline the brightness calculation instead of calling the static method
335+
- Brightness changes do NOT invalidate the LUT since brightness is applied after lookup
336+
218337
## Parameter Access
219338

220339
### Direct Virtual Member Assignment

lib/libesp32/berry_animation/src/core/animation_engine.be

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,14 +258,7 @@ class AnimationEngine
258258

259259
# Output frame buffer to LED strip
260260
def _output_to_strip()
261-
var i = 0
262-
var strip_length = self.strip_length
263-
var strip = self.strip
264-
var pixels = self.frame_buffer.pixels
265-
while i < strip_length
266-
strip.set_pixel_color(i, pixels.get(i * 4, 4))
267-
i += 1
268-
end
261+
self.strip.push_pixels_buffer_argb(self.frame_buffer.pixels)
269262
self.strip.show()
270263
end
271264

lib/libesp32/berry_animation/src/providers/color_cycle_color_provider.be

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,14 @@ class ColorCycleColorProvider : animation.color_provider
139139
if (idx >= palette_size) idx = palette_size - 1 end
140140
if (idx < 0) idx = 0 end
141141
self.current_index = idx
142-
return self._get_color_at_index(self.current_index)
142+
var color = self._get_color_at_index(self.current_index)
143+
144+
# Apply brightness scaling
145+
var brightness = self.brightness
146+
if brightness != 255
147+
return self.apply_brightness(color, brightness)
148+
end
149+
return color
143150
end
144151

145152
# 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
151158
color_index = palette_size - 1
152159
end
153160

154-
# Update current state and return the color
161+
# Update current state and get the color
155162
self.current_index = color_index
156-
return self._get_color_at_index(color_index)
163+
var color = self._get_color_at_index(color_index)
164+
165+
# Apply brightness scaling
166+
var brightness = self.brightness
167+
if brightness != 255
168+
return self.apply_brightness(color, brightness)
169+
end
170+
return color
157171
end
158172

159173
# Get a color based on a value (maps value to position in cycle)
@@ -170,7 +184,12 @@ class ColorCycleColorProvider : animation.color_provider
170184
end
171185

172186
if palette_size == 1
173-
return self._get_color_at_index(0) # If only one color, just return it
187+
var color = self._get_color_at_index(0) # If only one color, just return it
188+
var brightness = self.brightness
189+
if brightness != 255
190+
return self.apply_brightness(color, brightness)
191+
end
192+
return color
174193
end
175194

176195
# Clamp value to 0-255
@@ -188,7 +207,14 @@ class ColorCycleColorProvider : animation.color_provider
188207
color_index = palette_size - 1
189208
end
190209

191-
return self._get_color_at_index(color_index)
210+
var color = self._get_color_at_index(color_index)
211+
212+
# Apply brightness scaling
213+
var brightness = self.brightness
214+
if brightness != 255
215+
return self.apply_brightness(color, brightness)
216+
end
217+
return color
192218
end
193219

194220
# String representation of the provider

lib/libesp32/berry_animation/src/providers/color_provider.be

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@
1414

1515
#@ solidify:ColorProvider,weak
1616
class ColorProvider : animation.value_provider
17+
# LUT (Lookup Table) management for color providers
18+
# Subclasses can use this to cache pre-computed colors for performance
19+
# If a subclass doesn't use a LUT, this remains nil
20+
var _color_lut # Color lookup table cache (bytes() object or nil)
21+
var _lut_dirty # Flag indicating LUT needs rebuilding
22+
23+
# Parameter definitions
24+
static var PARAMS = animation.enc_params({
25+
"brightness": {"min": 0, "max": 255, "default": 255}
26+
})
27+
28+
# Initialize the color provider
29+
#
30+
# @param engine: AnimationEngine - Reference to the animation engine (required)
31+
def init(engine)
32+
super(self).init(engine)
33+
self._color_lut = nil
34+
self._lut_dirty = true
35+
end
36+
37+
# Get the color lookup table
38+
# Returns the LUT bytes() object if the provider uses one, or nil otherwise
39+
#
40+
# @return bytes|nil - The LUT bytes object or nil
41+
def get_lut()
42+
return self._color_lut
43+
end
44+
1745
# Produce a color value for any parameter name
1846
# This is the main method that subclasses should override
1947
#
@@ -34,6 +62,31 @@ class ColorProvider : animation.value_provider
3462
return self.produce_value("color", time_ms) # Default: use time-based color
3563
end
3664

65+
# Static method to apply brightness scaling to a color
66+
# Only performs scaling if brightness is not 255 (full brightness)
67+
#
68+
# @param color: int - Color in ARGB format (0xAARRGGBB)
69+
# @param brightness: int - Brightness level (0-255)
70+
# @return int - Color with brightness applied
71+
static def apply_brightness(color, brightness)
72+
# Skip scaling if brightness is full (255)
73+
if brightness == 255
74+
return color
75+
end
76+
77+
# Extract RGB components (preserve alpha channel)
78+
var r = (color >> 16) & 0xFF
79+
var g = (color >> 8) & 0xFF
80+
var b = color & 0xFF
81+
82+
# Scale each component by brightness
83+
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
84+
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
85+
b = tasmota.scale_uint(b, 0, 255, 0, brightness)
86+
87+
# Reconstruct color with scaled brightness (preserve alpha)
88+
return (color & 0xFF000000) | (r << 16) | (g << 8) | b
89+
end
3790

3891
end
3992

0 commit comments

Comments
 (0)