211 lines
7.2 KiB
Plaintext
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 } |