Tasmota/lib/libesp32/berry_animation/src/animations/twinkle.be

211 lines
7.2 KiB
Plaintext

# Twinkle animation effect for Berry Animation Framework
#
# This animation creates a twinkling stars effect with random lights
# appearing and fading at different positions with customizable density and timing.
import "./core/param_encoder" as encode_constraints
class twinkle : animation.animation
# NO instance variables for parameters - they are handled by the virtual parameter system
# Non-parameter instance variables only
var current_colors # bytes() buffer storing ARGB colors (4 bytes per pixel)
var last_update # Last update time for timing
var random_seed # Seed for random number generation
# Parameter definitions with constraints
static var PARAMS = animation.enc_params({
"color": {"default": 0xFFFFFFBB}, # slightly yellow stars
"density": {"min": 0, "max": 255, "default": 64},
"twinkle_speed": {"min": 1, "max": 5000, "default": 100},
"fade_speed": {"min": 0, "max": 255, "default": 180},
"min_brightness": {"min": 0, "max": 255, "default": 32},
"max_brightness": {"min": 0, "max": 255, "default": 255}
})
# Initialize a new Twinkle animation
#
# @param engine: AnimationEngine - The animation engine (REQUIRED)
def init(engine)
# Call parent constructor with engine only
super(self).init(engine)
# Initialize non-parameter instance variables only
self.current_colors = bytes() # Use bytes() buffer for ARGB colors (4 bytes per pixel)
self.last_update = 0
# Initialize random seed using engine time
self.random_seed = self.engine.time_ms % 65536
# Initialize buffer based on strip length from engine
self._initialize_arrays()
end
# Initialize buffer based on current strip length
def _initialize_arrays()
var strip_length = self.engine.strip_length
# Create new bytes() buffer for colors (4 bytes per pixel: ARGB)
# Alpha channel serves as the active state: alpha=0 means off, alpha>0 means active
self.current_colors.clear()
self.current_colors.resize(strip_length * 4)
# Initialize all pixels to off state (transparent = alpha 0)
var i = 0
while i < strip_length
self.current_colors.set(i * 4, 0x00000000, -4) # Transparent (alpha = 0)
i += 1
end
end
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "twinkle_speed"
# Handle twinkle_speed - can be Hz (1-20) or period in ms (50-5000)
if value >= 50 # Assume it's period in milliseconds
# Convert period (ms) to frequency (Hz): Hz = 1000 / ms
# Clamp to reasonable range 1-20 Hz
var hz = 1000 / value
if hz < 1
hz = 1
elif hz > 20
hz = 20
end
# Update the parameter with the converted value
self.set_param("twinkle_speed", hz)
end
end
end
# Simple pseudo-random number generator
# Uses a linear congruential generator for consistent results
def _random()
self.random_seed = (self.random_seed * 1103515245 + 12345) & 0x7FFFFFFF
return self.random_seed
end
# Get random number in range [0, max)
def _random_range(max)
if max <= 0
return 0
end
return self._random() % max
end
# Update animation state based on current time
#
# @param time_ms: int - Current time in milliseconds
def update(time_ms)
# Access parameters via virtual members
var twinkle_speed = self.twinkle_speed
# Check if it's time to update the twinkle simulation
# Update frequency is based on twinkle_speed (Hz)
var update_interval = 1000 / twinkle_speed # milliseconds between updates
if time_ms - self.last_update >= update_interval
self.last_update = time_ms
self._update_twinkle_simulation(time_ms)
end
end
# Update the twinkle simulation with alpha-based fading
def _update_twinkle_simulation(time_ms)
# Access parameters via virtual members (cache for performance)
var fade_speed = self.fade_speed
var density = self.density
var min_brightness = self.min_brightness
var max_brightness = self.max_brightness
var color = self.color
var strip_length = self.engine.strip_length
# Ensure buffer is properly sized
if self.current_colors.size() != strip_length * 4
self._initialize_arrays()
end
# Step 1: Fade existing twinkles by reducing alpha
var i = 0
while i < strip_length
var current_color = self.current_colors.get(i * 4, -4)
var alpha = (current_color >> 24) & 0xFF
if alpha > 0
# Calculate fade amount based on fade_speed
var fade_amount = tasmota.scale_uint(fade_speed, 0, 255, 1, 20)
if alpha <= fade_amount
# Star has faded completely - reset to transparent
self.current_colors.set(i * 4, 0x00000000, -4)
else
# Reduce alpha while keeping RGB components unchanged
var new_alpha = alpha - fade_amount
var rgb = current_color & 0x00FFFFFF # Keep RGB, clear alpha
self.current_colors.set(i * 4, (new_alpha << 24) | rgb, -4)
end
end
i += 1
end
# Step 2: Randomly create new twinkles based on density
# For each pixel, check if it should twinkle based on density probability
var j = 0
while j < strip_length
# Only consider pixels that are currently off (alpha = 0)
var current_color = self.current_colors.get(j * 4, -4)
var alpha = (current_color >> 24) & 0xFF
if alpha == 0
# Use density as probability out of 255
if self._random_range(255) < density
# Create new star at full brightness with random intensity alpha
var star_alpha = min_brightness + self._random_range(max_brightness - min_brightness + 1)
# Get base color (automatically resolves value_providers)
var base_color = color
# Extract RGB components (ignore original alpha)
var r = (base_color >> 16) & 0xFF
var g = (base_color >> 8) & 0xFF
var b = base_color & 0xFF
# Create new star with full-brightness color and variable alpha
self.current_colors.set(j * 4, (star_alpha << 24) | (r << 16) | (g << 8) | b, -4)
end
end
j += 1
end
end
# Render the twinkle to the provided frame buffer
#
# @param frame: frame_buffer - The frame buffer to render to
# @param time_ms: int - Current time in milliseconds
# @param strip_length: int - Length of the LED strip in pixels
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms, strip_length)
# Ensure buffer is properly sized
if self.current_colors.size() != strip_length * 4
self._initialize_arrays()
end
# Only render pixels that are actually twinkling (non-transparent)
var modified = false
var i = 0
while i < strip_length
if i < frame.width
var color = self.current_colors.get(i * 4, -4)
# Only set pixels that have some alpha (are visible)
if (color >> 24) & 0xFF > 0
frame.set_pixel_color(i, color)
modified = true
end
end
i += 1
end
return modified
end
end
return { 'twinkle': twinkle }