/** * Berry Modules for Virtual Filesystem * * Total modules: 59 * * This file registers all Berry animation framework modules * in the browser's virtual filesystem for use with `import`. * * Load order: * 1. virtual-fs.js (creates window.virtualFS) * 2. berry-modules.js (this file - registers modules) * 3. berry.js (Berry WASM module) */ (function() { 'use strict'; // Ensure virtualFS is available if (typeof window.virtualFS === 'undefined') { console.error('[BerryModules] Error: window.virtualFS not found. Load virtual-fs.js first.'); return; } const modules = {}; modules["animation.be"] = "# Berry Animation Framework - Main Entry Point\n# \n# This is the central module that imports and registers all animation framework components\n# into a unified \"animation\" object for use in Tasmota LED strip control.\n#\n# The framework provides:\n# - DSL (Domain Specific Language) for declarative animation definitions \n# - Value providers for dynamic parameters (oscillators, color providers)\n# - Event system for interactive animations\n# - Optimized performance for embedded ESP32 systems\n#\n# Usage in Tasmota:\n# import animation\n# var engine = animation.create_engine(strip)\n# var pulse_anim = animation.pulse(animation.solid(0xFF0000), 2000, 50, 255)\n# engine.add(pulse_anim).start()\n#\n# Launch standalone with: \"./berry -s -g -m lib/libesp32/berry_animation\"\n\n# Import Tasmota integration if available (for embedded use)\nimport global\nif !global.contains(\"tasmota\")\n import tasmota\nend\n\n# Create the main animation module and make it globally accessible\n# The @solidify directive enables compilation to C++ for performance\n#@ solidify:animation,weak\nvar animation = module(\"animation\")\nglobal.animation = animation\n\n# Version information for compatibility tracking\n# Format: 0xAABBCCDD (AA=major, BB=minor, CC=patch, DD=build)\nanimation.VERSION = 0x00010000\n\nimport sys\n\n# Helper function to register all exports from imported modules into the main animation object\n# This creates a flat namespace where all animation functions are accessible as animation.function_name()\n# Takes a map returned by \"import XXX\" and adds each key/value to module `animation`\ndef register_to_animation(m)\n for k: m.keys()\n animation.(k) = m[k]\n end\nend\n\n# Import core framework components\n# These provide the fundamental architecture for the animation system\n\n# Parameter constraint encoder for PARAMS definitions\nimport \"core/param_encoder\" as param_encoder\nregister_to_animation(param_encoder)\n\n# Mathematical functions for use in closures and throughout the framework\nimport \"core/math_functions\" as math_functions\nregister_to_animation(math_functions)\n\n# Base class for parameter management and playable behavior - shared by Animation and ValueProvider\nimport \"core/parameterized_object\" as parameterized_object\nregister_to_animation(parameterized_object)\n\n# Frame buffer management for LED strip pixel data\nimport \"core/frame_buffer\" as frame_buffer\nregister_to_animation(frame_buffer)\n\n# Base Animation class - unified foundation for all visual elements\nimport \"core/animation_base\" as animation_base\nregister_to_animation(animation_base)\n\n# Sequence manager for complex animation choreography\nimport \"core/sequence_manager\" as sequence_manager\nregister_to_animation(sequence_manager)\n\n# Engine proxy - combines rendering and orchestration\nimport \"core/engine_proxy\" as engine_proxy\nregister_to_animation(engine_proxy)\n\n# Unified animation engine - central engine for all animations\n# Provides priority-based layering, automatic blending, and performance optimization\nimport \"core/animation_engine\" as animation_engine\nregister_to_animation(animation_engine)\n\n# Event system for interactive animations (button presses, timers, etc.)\nimport \"core/event_handler\" as event_handler\nregister_to_animation(event_handler)\n\n# User-defined function registry for DSL extensibility\nimport \"core/user_functions\" as user_functions\nregister_to_animation(user_functions)\n\n# Import and register actual user functions\n# try\n# import \"user_functions\" as user_funcs # This registers the actual user functions\n# except .. as e, msg\n# # User functions are optional - continue without them if not available\n# print(f\"Note: User functions not loaded: {msg}\")\n# end\n\n# Import value providers\nimport \"providers/value_provider.be\" as value_provider\nregister_to_animation(value_provider)\nimport \"providers/static_value_provider.be\" as static_value_provider\nregister_to_animation(static_value_provider)\nimport \"providers/oscillator_value_provider.be\" as oscillator_value_provider\nregister_to_animation(oscillator_value_provider)\nimport \"providers/strip_length_provider.be\" as strip_length_provider\nregister_to_animation(strip_length_provider)\nimport \"providers/iteration_number_provider.be\" as iteration_number_provider\nregister_to_animation(iteration_number_provider)\nimport \"providers/closure_value_provider.be\" as closure_value_provider\nregister_to_animation(closure_value_provider)\n\n# Import color providers\nimport \"providers/color_provider.be\" as color_provider\nregister_to_animation(color_provider)\nimport \"providers/color_cycle_color_provider.be\" as color_cycle_color_provider\nregister_to_animation(color_cycle_color_provider)\n# import \"providers/composite_color_provider.be\" as composite_color_provider\n# register_to_animation(composite_color_provider)\nimport \"providers/rich_palette_color_provider.be\" as rich_palette_color_provider\nregister_to_animation(rich_palette_color_provider)\nimport \"providers/breathe_color_provider.be\" as breathe_color_provider\nregister_to_animation(breathe_color_provider)\n\n# Import animations\nimport \"animations/solid\" as solid_impl\nregister_to_animation(solid_impl)\nimport \"animations/beacon\" as beacon\nregister_to_animation(beacon)\nimport \"animations/crenel\" as crenel\nregister_to_animation(crenel)\nimport \"animations/breathe\" as breathe\nregister_to_animation(breathe)\nimport \"animations/palette_gradient\" as palette_pattern_animation\nregister_to_animation(palette_pattern_animation)\nimport \"animations/comet\" as comet\nregister_to_animation(comet)\n# import \"animations/fire\" as fire\n# register_to_animation(fire)\nimport \"animations/twinkle\" as twinkle\nregister_to_animation(twinkle)\nimport \"animations/gradient\" as gradient\nregister_to_animation(gradient)\nimport \"animations/palette_meter\" as palette_meter\nregister_to_animation(palette_meter)\n# import \"animations/plasma\" as plasma_animation\n# register_to_animation(plasma_animation)\n# import \"animations/sparkle\" as sparkle_animation\n# register_to_animation(sparkle_animation)\n# import \"animations/wave\" as wave\n# register_to_animation(wave)\n# import \"animations/shift\" as shift_animation\n# register_to_animation(shift_animation)\n# import \"animations/bounce\" as bounce_animation\n# register_to_animation(bounce_animation)\n# import \"animations/scale\" as scale_animation\n# register_to_animation(scale_animation)\n# import \"animations/jitter\" as jitter_animation\n# register_to_animation(jitter_animation)\n\n# Import palette examples\nimport \"animations/palettes\" as palettes\nregister_to_animation(palettes)\n\n# Import specialized animation classes\nimport \"animations/rich_palette\" as rich_palette\nregister_to_animation(rich_palette)\n\n# DSL components are now in separate animation_dsl module\n\n# Function called to initialize the `Leds` and `engine` objects\n#\n# It keeps track of previously created engines and strips to reuse\n# when called with the same arguments\n#\n# Parameters:\n# l - list of arguments (vararg)\n#\n# Returns:\n# An instance of `AnimationEngine` managing the strip\ndef animation_init_strip(*l)\n import global\n import animation\n import introspect\n # we keep a hash of strip configurations to reuse existing engines\n if !introspect.contains(animation, \"_engines\")\n animation._engines = {}\n end\n\n var l_as_string = str(l)\n var engine = animation._engines.find(l_as_string)\n if (engine != nil)\n # we reuse it\n engine.stop()\n engine.clear()\n else\n var strip = call(global.Leds, l) # call global.Leds() with vararg\n engine = animation.create_engine(strip)\n animation._engines[l_as_string] = engine\n end\n\n return engine\nend\nanimation.init_strip = animation_init_strip\n\n# This function is called from C++ code to set up the Berry animation environment\n# It creates a mutable 'animation' module on top of the immutable solidified\n#\n# Parameters:\n# m - Solidified immutable module\n#\n# Returns:\n# A new animation module instance that is return for `import animation`\ndef animation_init(m)\n var animation_new = module(\"animation\") # Create new non-solidified module for runtime use\n animation_new._ntv = m # Keep reference to native solidified module\n animation_new.event_manager = m.EventManager() # Create event manager instance for handling triggers\n \n # Create dynamic member lookup function for extensibility\n # This allows the module to find members in both Berry and solidified components\n #\n # Note: if the module already contained the member, then `member()` would not be called in the first place\n animation_new.member = def (k)\n import animation\n import introspect\n if introspect.contains(animation._ntv, k)\n return animation._ntv.(k) # Return native solidified member if available\n else\n return module(\"undefined\") # Return undefined module for missing members\n end\n end\n\n # Create an empty map for user_functions\n animation_new._user_functions = {}\n\n return animation_new\nend\nanimation.init = animation_init\n\nreturn animation\n"; modules["animation_dsl.be"] = "# Berry Animation Framework - DSL Module\n# \n# This module provides Domain-Specific Language (DSL) functionality for the\n# Berry Animation Framework. It allows users to write animations using a\n# declarative syntax that gets transpiled to Berry code.\n#\n# The DSL provides:\n# - Declarative animation definitions with intuitive syntax\n# - Color and palette definitions\n# - Animation sequences and timing control\n# - Property assignments and dynamic parameters\n# - Event system integration\n# - User-defined functions\n#\n# Usage:\n# import animation_dsl\n# var berry_code = animation_dsl.compile(dsl_source)\n# animation_dsl.execute(berry_code)\n#\n\nimport global\nimport animation\n\n# Requires to first `import animation`\n# We don't include it to not create a closure, but use the global instead\n\n# Create the DSL module and make it globally accessible\n#@ solidify:animation_dsl.SimpleDSLTranspiler.ExpressionResult,weak\n#@ solidify:animation_dsl,weak\nvar animation_dsl = module(\"animation_dsl\")\nglobal.animation_dsl = animation_dsl\n\n# Helper function to register all exports from imported modules into the DSL module\ndef register_to_dsl(m)\n for k: m.keys()\n animation_dsl.(k) = m[k]\n end\nend\n\n# Import DSL components\nimport \"dsl/token.be\" as dsl_token\nregister_to_dsl(dsl_token)\nimport \"dsl/lexer.be\" as dsl_lexer\nregister_to_dsl(dsl_lexer)\nimport \"dsl/transpiler.be\" as dsl_transpiler\nregister_to_dsl(dsl_transpiler)\nimport \"dsl/symbol_table.be\" as dsl_symbol_table\nregister_to_dsl(dsl_symbol_table)\nimport \"dsl/named_colors.be\" as dsl_named_colors\nregister_to_dsl(dsl_named_colors)\n\n# Import Web UI components\nimport \"webui/animation_web_ui.be\" as animation_web_ui\nregister_to_dsl(animation_web_ui)\n\nimport \"dsl/all_wled_palettes\" as all_wled_palettes\nregister_to_dsl(all_wled_palettes)\n\n# Main DSL compilation function\n# Compiles DSL source code to Berry code\n#\n# @param source: string - DSL source code\n# @return string - Generated Berry code\ndef compile_dsl_source(source)\n import animation_dsl\n return animation_dsl.compile_dsl(source)\nend\nanimation_dsl.compile = compile_dsl_source\n\n# Execute DSL source code\n# Compiles and executes DSL source in one step\n#\n# @param source: string - DSL source code\n# @return any - Result of execution\ndef execute(source)\n import animation_dsl\n var berry_code = animation_dsl.compile(source)\n var compiled_fn = compile(berry_code)\n return compiled_fn()\nend\nanimation_dsl.execute = execute\n\n# Load and execute DSL from file\n#\n# @param filename: string - Path to DSL file\n# @return any - Result of execution\ndef load_file(filename)\n import animation_dsl\n var f = open(filename, \"r\")\n if f == nil\n raise \"io_error\", f\"Cannot open DSL file: {filename}\"\n end\n \n var source = f.read()\n f.close()\n \n return animation_dsl.execute(source)\nend\nanimation_dsl.load_file = load_file\n\n# Compile .anim file to .be file\n# Takes a filename with .anim suffix and compiles to same prefix with .be suffix\n#\n# @param filename: string - Path to .anim file\n# @return bool - True if compilation successful\n# @raises \"io_error\" - If file cannot be read or written\n# @raises \"dsl_compilation_error\" - If DSL compilation fails\n# @raises \"invalid_filename\" - If filename doesn't have .anim extension\ndef compile_file(filename)\n import string\n import animation_dsl\n \n # Validate input filename\n if !string.endswith(filename, \".anim\")\n raise \"invalid_filename\", f\"Input file must have .anim extension: {filename}\"\n end\n \n # Generate output filename\n var base_name = filename[0..-6] # Remove .anim extension (5 chars + 1 for 0-based)\n var output_filename = base_name + \".be\"\n \n # Read DSL source\n var f = open(filename, \"r\")\n if f == nil\n raise \"io_error\", f\"Cannot open input file: {filename}\"\n end\n \n var dsl_source = f.read()\n f.close()\n \n # Compile DSL to Berry code\n var berry_code = animation_dsl.compile(dsl_source)\n if berry_code == nil\n raise \"dsl_compilation_error\", f\"DSL compilation failed for: {filename}\"\n end\n \n # Generate header with metadata (no original source for compile_file)\n var header = \"# Generated Berry code from Animation DSL\\n\" +\n f\"# Source: {filename}\\n\" +\n \"# Generated automatically by animation_dsl.compile_file()\\n\" +\n \"# \\n\" +\n \"# Do not edit manually - changes will be overwritten\\n\" +\n \"\\n\"\n \n # Write complete Berry file (no footer with original source)\n var output_f = open(output_filename, \"w\")\n if output_f == nil\n raise \"io_error\", f\"Cannot create output file: {output_filename}\"\n end\n \n output_f.write(header + berry_code)\n output_f.close()\n \n return true\nend\nanimation_dsl.compile_file = compile_file\n\n# this function is called when the module is loaded\ndef animation_dsl_init(m)\n import animation\n # load the Web UI component\n var animation_web_ui = m.animation_web_ui\n animation.web_ui = animation_web_ui() # create an instance and store in \"animation.web_ui\"\n\n return m # return the module unchanged\nend\nanimation_dsl.init = animation_dsl_init\n\nreturn animation_dsl\n"; modules["animations/beacon.be"] = "# Beacon animation effect for Berry Animation Framework\n#\n# This animation creates a beacon effect at a specific position on the LED strip.\n# It displays a color beacon with optional slew (fade) regions on both sides.\n#\n# Beacon diagram (right_edge=0, default, left edge):\n# pos (1)\n# |\n# v\n# _______\n# / \\\n# _______/ \\____________\n# | | | |\n# |2| 3 |2|\n#\n# Beacon diagram (right_edge=1, right edge):\n# pos (1)\n# |\n# v\n# _______\n# / \\\n# _______/ \\_________\n# | | | |\n# |2| 3 |2|\n#\n# 1: `pos`, position of the beacon edge (left edge for right_edge=0, right edge for right_edge=1)\n# 2: `slew_size`, number of pixels to fade from back to fore color, can be `0`\n# 3: `beacon_size`, number of pixels of the beacon\n# When right_edge=1, pos=0 shows 1 pixel at the right edge (rightmost pixel of strip)\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass beacon : animation.animation\n # NO instance variables for parameters - they are handled by the virtual parameter system\n \n # Parameter definitions following the new specification\n static var PARAMS = animation.enc_params({\n \"back_color\": {\"default\": 0xFF000000},\n \"pos\": {\"default\": 0},\n \"beacon_size\": {\"min\": 0, \"default\": 1},\n \"slew_size\": {\"min\": 0, \"default\": 0},\n \"right_edge\": {\"enum\": [0, 1], \"default\": 0}\n })\n\n # Render the beacon to the provided frame buffer\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Use virtual parameter access - automatically resolves value_providers\n var member = self.member\n var back_color = member(self, \"back_color\")\n var pos = member(self, \"pos\")\n var slew_size = member(self, \"slew_size\")\n var beacon_size = member(self, \"beacon_size\")\n var color = member(self, \"color\")\n var right_edge = member(self, \"right_edge\")\n \n # Fill background if not transparent\n if (back_color != 0xFF000000) && ((back_color & 0xFF000000) != 0x00)\n frame.fill_pixels(frame.pixels, back_color)\n end\n \n # Calculate effective position based on right_edge\n # right_edge=0: pos is the left edge of the beacon (default)\n # right_edge=1: pos is the right edge of the beacon (from right side of strip)\n var effective_pos\n if right_edge == 1\n # Right edge mode: pos indicates right edge of beacon from right side of strip\n # effective_pos is the left edge of the beacon in absolute coordinates\n effective_pos = pos - beacon_size + 1\n else\n effective_pos = pos\n end\n \n # Calculate beacon boundaries\n var beacon_min = effective_pos\n var beacon_max = effective_pos + beacon_size\n \n # Clamp to frame boundaries\n if beacon_min < 0\n beacon_min = 0\n end\n if beacon_max >= strip_length\n beacon_max = strip_length\n end\n \n # Draw the main beacon\n frame.fill_pixels(frame.pixels, color, beacon_min, beacon_max)\n var i\n \n # Draw slew regions if slew_size > 0\n if slew_size > 0\n # Left slew (fade from background to beacon color)\n var left_slew_min = effective_pos - slew_size\n var left_slew_max = effective_pos\n \n if left_slew_min < 0\n left_slew_min = 0\n end\n if left_slew_max >= strip_length\n left_slew_max = strip_length\n end\n \n i = left_slew_min\n while i < left_slew_max\n # Calculate blend factor - blend from 255 (back) to 0 (fore) like original\n var blend_factor = tasmota.scale_int(i, effective_pos - slew_size - 1, effective_pos, 255, 0)\n var blended_color = frame.blend_linear(back_color, color, blend_factor)\n frame.set_pixel_color(i, blended_color)\n i += 1\n end\n \n # Right slew (fade from beacon color to background)\n var right_slew_min = effective_pos + beacon_size\n var right_slew_max = effective_pos + beacon_size + slew_size\n \n if right_slew_min < 0\n right_slew_min = 0\n end\n if right_slew_max >= strip_length\n right_slew_max = strip_length\n end\n \n i = right_slew_min\n while i < right_slew_max\n # Calculate blend factor - blend from 0 (fore) to 255 (back) like original\n var blend_factor = tasmota.scale_int(i, effective_pos + beacon_size - 1, effective_pos + beacon_size + slew_size, 0, 255)\n var blended_color = frame.blend_linear(back_color, color, blend_factor)\n frame.set_pixel_color(i, blended_color)\n i += 1\n end\n end\n \n return true\n end\nend\n\n# Export class directly - no redundant factory function needed\nreturn {'beacon': beacon}\n"; modules["animations/breathe.be"] = "# Breathe animation effect for Berry Animation Framework\n#\n# This animation creates a breathing/pulsing effect that oscillates between a minimum and maximum brightness.\n# It supports different curve patterns from simple sine waves to natural breathing with pauses.\n# It's useful for creating both smooth pulsing effects and calming, organic lighting effects.\n#\n# The effect uses a breathe_color_provider internally to generate the breathing color effect.\n# - curve_factor 1: Pure cosine wave (equivalent to pulse animation)\n# - curve_factor 2-5: Natural breathing with pauses at peaks (5 = most pronounced pauses)\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass breathe : animation.animation\n # Non-parameter instance variables only\n var _breathe # Internal breathe color provider\n \n # Parameter definitions following parameterized class specification\n # Note: 'color' is inherited from Animation base class\n static var PARAMS = animation.enc_params({\n \"min_brightness\": {\"min\": 0, \"max\": 255, \"default\": 0}, # Minimum brightness level (0-255)\n \"max_brightness\": {\"min\": 0, \"max\": 255, \"default\": 255}, # Maximum brightness level (0-255)\n \"period\": {\"min\": 100, \"default\": 3000}, # Time for one complete breathe cycle in milliseconds\n \"curve_factor\": {\"min\": 1, \"max\": 5, \"default\": 2} # Factor to control breathing curve shape (1=cosine wave, 2-5=curved breathing with pauses)\n })\n \n # Initialize a new Breathe animation\n # Following parameterized class specification - engine parameter only\n #\n # @param engine: AnimationEngine - The animation engine (required)\n def init(engine)\n # Call parent constructor with engine parameter only\n super(self).init(engine)\n \n # Create internal breathe color provider\n self._breathe = animation.breathe_color(engine)\n \n # Set the animation's color parameter to use the breathe provider\n self.values[\"color\"] = self._breathe\n end\n \n # Handle parameter changes - propagate to internal breathe provider\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n # Propagate relevant parameters to the breathe provider\n if name == \"color\"\n # When color is set, update the _breathe's color\n # but keep the _breathe as the actual color source for rendering\n if type(value) == 'int'\n self._breathe.color = value\n # Restore the _breathe as the color source (bypass on_param_changed)\n self.values[\"color\"] = self._breathe\n end\n elif name == \"min_brightness\"\n self._breathe.min_brightness = value\n elif name == \"max_brightness\"\n self._breathe.max_brightness = value\n elif name == \"period\"\n self._breathe.period = value\n elif name == \"curve_factor\"\n self._breathe.curve_factor = value\n end\n end\n \n # Override start method to synchronize the internal provider\n #\n # @param start_time: int - Optional start time in milliseconds\n # @return self for method chaining\n def start(start_time)\n # Call parent start method first\n super(self).start(start_time)\n self._breathe.start(start_time)\n return self\n end\n \n # The render method is inherited from Animation base class\n # It automatically uses self.color (which is set to self._breathe)\n # The _breathe produces the breathing color effect\nend\n\nreturn {'breathe': breathe }"; modules["animations/comet.be"] = "# Comet animation effect for Berry Animation Framework\n#\n# This animation creates a comet effect with a bright head and a fading tail.\n# The comet moves across the LED strip with customizable speed, length, and direction.\n#\n# The comet uses sub-pixel positioning (1/256th pixels) for smooth movement and supports\n# both wrapping around the strip and bouncing off the ends.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass comet : animation.animation\n # Non-parameter instance variables only\n var head_position # Current position of the comet head (in 1/256th pixels for smooth movement)\n \n # Parameter definitions following parameterized class specification\n static var PARAMS = animation.enc_params({\n # 'color' for the comet head (32-bit ARGB value), inherited from animation class\n \"tail_length\": {\"min\": 1, \"max\": 50, \"default\": 5}, # Length of the comet tail in pixels\n \"speed\": {\"min\": 1, \"max\": 25600, \"default\": 2560}, # Movement speed in 1/256th pixels per second\n \"direction\": {\"enum\": [-1, 1], \"default\": 1}, # Direction of movement (1 = forward, -1 = backward)\n \"wrap_around\": {\"min\": 0, \"max\": 1, \"default\": 1}, # Whether comet wraps around the strip (bool)\n \"fade_factor\": {\"min\": 0, \"max\": 255, \"default\": 179} # How quickly the tail fades (0-255, 255 = no fade)\n })\n \n # Initialize a new Comet animation\n # Following parameterized class specification - engine parameter only\n #\n # @param engine: AnimationEngine - The animation engine (required)\n def init(engine)\n # Call parent constructor with engine parameter only\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n # Initialize position based on default direction (forward = start at beginning)\n self.head_position = 0\n end\n \n # Handle parameter changes - reset position when direction changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"direction\"\n # Reset position when direction changes\n var strip_length = self.engine.strip_length\n if type(value) == 'int'\n if value > 0\n self.head_position = 0 # Start at beginning for forward movement\n else\n self.head_position = (strip_length - 1) * 256 # Start at end for backward movement\n end\n end\n end\n end\n \n # Update animation state based on current time\n #\n # @param time_ms: int - current time in milliseconds\n def update(time_ms)\n # Cache parameter values for performance (read once, use multiple times)\n var current_speed = self.speed\n var current_direction = self.direction\n var current_wrap_around = self.wrap_around\n var strip_length = self.engine.strip_length\n \n # Calculate elapsed time since animation started\n var elapsed = time_ms - self.start_time\n \n # Calculate movement based on elapsed time and speed\n # speed is in 1/256th pixels per second, elapsed is in milliseconds\n # distance = (speed * elapsed_ms) / 1000\n var distance_moved = (current_speed * elapsed * current_direction) / 1000\n \n # Update head position\n if current_direction > 0\n self.head_position = distance_moved\n else\n self.head_position = ((strip_length - 1) * 256) + distance_moved\n end\n \n # Handle wrapping or bouncing (convert to pixel boundaries)\n var strip_length_subpixels = strip_length * 256\n if current_wrap_around != 0\n # Wrap around the strip\n while self.head_position >= strip_length_subpixels\n self.head_position -= strip_length_subpixels\n end\n while self.head_position < 0\n self.head_position += strip_length_subpixels\n end\n else\n # Bounce off the ends\n if self.head_position >= strip_length_subpixels\n self.head_position = (strip_length - 1) * 256\n # Update direction parameter using virtual member assignment\n self.direction = -current_direction\n elif self.head_position < 0\n self.head_position = 0\n # Update direction parameter using virtual member assignment\n self.direction = -current_direction\n end\n end\n end\n \n # Render the comet to the provided frame buffer\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Get the integer position of the head (convert from 1/256th pixels to pixels)\n var head_pixel = self.head_position / 256\n \n # Get current parameter values using virtual member access (resolves value_providers automatically)\n var current_color = self.color\n var tail_length = self.tail_length\n var direction = self.direction\n var wrap_around = self.wrap_around\n var fade_factor = self.fade_factor\n \n # Extract color components from current color (ARGB format)\n var head_a = (current_color >> 24) & 0xFF\n var head_r = (current_color >> 16) & 0xFF\n var head_g = (current_color >> 8) & 0xFF\n var head_b = current_color & 0xFF\n \n # Render the comet head and tail\n var i = 0\n while i < tail_length\n var pixel_pos = head_pixel - (i * direction)\n \n # Handle wrapping for pixel position\n if wrap_around != 0\n while pixel_pos >= strip_length\n pixel_pos -= strip_length\n end\n while pixel_pos < 0\n pixel_pos += strip_length\n end\n else\n # Skip pixels outside the strip\n if pixel_pos < 0 || pixel_pos >= strip_length\n i += 1\n continue\n end\n end\n \n # Calculate alpha based on distance from head (alpha-based fading)\n var alpha = 255 # Start at full alpha for head\n if i > 0\n # Use fade_factor to calculate exponential alpha decay\n var j = 0\n while j < i\n alpha = tasmota.scale_uint(alpha, 0, 255, 0, fade_factor)\n j += 1\n end\n end\n \n # Keep RGB components at full brightness, only fade via alpha\n # This creates a more realistic comet tail that fades to transparent\n var pixel_color = (alpha << 24) | (head_r << 16) | (head_g << 8) | head_b\n \n # Set the pixel in the frame buffer\n if pixel_pos >= 0 && pixel_pos < frame.width\n frame.set_pixel_color(pixel_pos, pixel_color)\n end\n \n i += 1\n end\n \n return true\n end\nend\n\nreturn {'comet': comet}\n"; modules["animations/crenel.be"] = "# Crenel Position animation effect for Berry Animation Framework\n#\n# This animation creates a crenel (square wave) effect at a specific position on the LED strip.\n# It displays repeating rectangular pulses with configurable spacing and count.\n#\n# Crenel diagram:\n# pos (1)\n# |\n# v (*4)\n# ______ ____\n# | | |\n# _________| |_________|\n# \n# | 2 | 3 |\n#\n# 1: `pos`, start of the pulse (in pixel)\n# 2: `pulse_size`, number of pixels of the pulse\n# 3: `low_size`, number of pixel until next pos - full cycle is 2 + 3\n# 4: `nb_pulse`, number of pulses, or `-1` for infinite\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass crenel : animation.animation\n # NO instance variables for parameters - they are handled by the virtual parameter system\n \n # Parameter definitions with constraints\n static var PARAMS = animation.enc_params({\n # 'color' for the comet head (32-bit ARGB value), inherited from animation class\n \"back_color\": {\"default\": 0x00000000}, # background color (transparent by default)\n \"pos\": {\"default\": 0}, # start of the pulse (in pixel)\n \"pulse_size\": {\"min\": 0, \"default\": 1}, # number of pixels of the pulse\n \"low_size\": {\"min\": 0, \"default\": 3}, # number of pixel until next pos - full cycle is 2 + 3\n \"nb_pulse\": {\"default\": -1} # number of pulses, or `-1` for infinite\n })\n \n # Render the crenel pattern to the provided frame buffer\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Access parameters via virtual members (automatically resolves value_providers)\n var back_color = self.back_color\n var pos = self.pos\n var pulse_size = self.pulse_size\n var low_size = self.low_size\n var nb_pulse = self.nb_pulse\n var color = self.color\n \n var period = int(pulse_size + low_size)\n \n # Fill background if not transparent\n if back_color != 0x00000000\n frame.fill_pixels(frame.pixels, back_color)\n end\n \n # Ensure we have a meaningful period\n if period <= 0\n period = 1\n end\n \n # Nothing to paint if nb_pulse is 0\n if nb_pulse == 0\n return true\n end\n \n # For infinite pulses, optimize starting position\n if nb_pulse < 0\n # Find the position of the first visible falling range (pos + pulse_size - 1)\n pos = ((pos + pulse_size - 1) % period) - pulse_size + 1\n else\n # For finite pulses, skip periods that are completely before the visible area\n while (pos < -period) && (nb_pulse != 0)\n pos += period\n nb_pulse -= 1\n end\n end\n \n # Render pulses\n while (pos < strip_length) && (nb_pulse != 0)\n var i = 0\n if pos < 0\n i = -pos\n end\n # Invariant: pos + i >= 0\n \n # Draw the pulse pixels\n while (i < pulse_size) && (pos + i < strip_length)\n frame.set_pixel_color(pos + i, color)\n i += 1\n end\n \n # Move to next pulse position\n pos += period\n nb_pulse -= 1\n end\n \n return true\n end\nend\n\nreturn {'crenel': crenel}\n"; modules["animations/gradient.be"] = "# Gradient animation effect for Berry Animation Framework\n#\n# Creates smooth color gradients between two colors.\n# Reimplemented as a subclass of beacon for simplicity.\n#\n# Parameters:\n# - color1: First color (default: red 0xFFFF0000)\n# - color2: Second color (default: blue 0xFF0000FF)\n# - gradient_type: 0=linear (two-point), 1=radial (center-to-edges)\n# - direction: 0=forward (color1 to color2), 1=reverse (color2 to color1)\n#\n# Implementation:\n# - Linear gradient: Left slew of a large beacon positioned at the right edge\n# - Radial gradient: Centered beacon of size=1 with slew_size=strip_length/2\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass gradient : animation.beacon\n # Parameter definitions - gradient-specific parameters\n static var PARAMS = animation.enc_params({\n \"color1\": {\"default\": 0xFFFF0000}, # First color (default red)\n \"color2\": {\"default\": 0xFF0000FF}, # Second color (default blue)\n \"direction\": {\"enum\": [0, 1], \"default\": 0}, # 0=forward, 1=reverse\n \"gradient_type\": {\"enum\": [0, 1], \"default\": 0} # 0=linear, 1=radial\n })\n \n # Override render to dynamically configure beacon based on strip_length and gradient parameters\n def render(frame, time_ms, strip_length)\n var col1 = self.color1\n var col2 = self.color2\n var direction = self.direction\n var gradient_type = self.gradient_type\n if direction\n self.color = col2\n self.back_color = col1\n else\n self.color = col1\n self.back_color = col2\n end\n \n if gradient_type\n # Radial gradient: centered beacon, color at center, back_color at edges\n var center = (strip_length - 1) / 2\n self.pos = center\n self.beacon_size = 1 + (1 - strip_length & 1)\n self.slew_size = (center > 0) ? center - 1 : 0\n self.right_edge = 0\n else\n # Linear gradient: right slew of a large beacon at left edge\n self.pos = 0\n self.beacon_size = 1000\n self.slew_size = (strip_length > 1) ? strip_length - 2 : 0\n self.right_edge = 1\n end\n \n return super(self).render(frame, time_ms, strip_length)\n end\nend\n\nreturn { 'gradient': gradient }"; modules["animations/palette_gradient.be"] = "# PaletteGradient animation effect for Berry Animation Framework\n#\n# This animation creates gradient patterns with palette colors.\n# It supports shifting gradients, spatial periods, and phase shifts.\n\nimport \"./core/param_encoder\" as encode_constraints\n\n# Gradient pattern animation - creates shifting gradient patterns\nclass palette_gradient : animation.animation\n var value_buffer # Buffer to store values for each pixel (bytes object)\n var _spatial_period # Cached spatial_period for static pattern optimization\n var _phase_shift # Cached phase_shift for static pattern optimization\n \n # Static definitions of parameters with constraints\n static var PARAMS = animation.enc_params({\n # Gradient-specific parameters\n \"color_source\": {\"default\": nil, \"type\": \"instance\"},\n \"shift_period\": {\"min\": 0, \"default\": 0}, # Time for one complete shift cycle in ms (0 = static)\n \"spatial_period\": {\"min\": 0, \"default\": 0}, # Spatial period in pixels (0 = full strip)\n \"phase_shift\": {\"min\": 0, \"max\": 255, \"default\": 0} # Phase shift in 0-255 range\n })\n \n # Initialize a new gradient pattern animation\n #\n # @param engine: AnimationEngine - Required animation engine reference\n def init(engine)\n # Call parent constructor with engine\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.value_buffer = bytes()\n \n # Initialize value buffer with default frame width\n self._initialize_value_buffer()\n end\n \n # Initialize the value buffer based on current strip length\n def _initialize_value_buffer()\n var strip_length = self.engine.strip_length\n self.value_buffer.resize(strip_length)\n \n # Initialize with zeros\n var i = 0\n while i < strip_length\n self.value_buffer[i] = 0\n i += 1\n end\n end\n \n # Update the value buffer to generate gradient pattern\n def _update_value_buffer(time_ms, strip_length)\n # Cache parameter values for performance\n var shift_period = self.member(\"shift_period\")\n var spatial_period = self.member(\"spatial_period\")\n var phase_shift = self.member(\"phase_shift\")\n \n # Optimization: for static patterns (shift_period == 0), skip recomputation\n # if spatial_period, phase_shift, and strip_length haven't changed\n if shift_period == 0\n if self._spatial_period != nil &&\n self._spatial_period == spatial_period &&\n self._phase_shift == phase_shift &&\n size(self.value_buffer) == strip_length\n return # No changes, skip recomputation\n end\n # Update cached values\n self._spatial_period = spatial_period\n self._phase_shift = phase_shift\n end\n \n # Determine effective spatial period (0 means full strip)\n var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length\n \n # Calculate the temporal shift position (how much the pattern has moved over time)\n var temporal_offset = 0\n if shift_period > 0\n temporal_offset = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, effective_spatial_period)\n end\n \n # Calculate the phase shift offset in pixels\n var phase_offset = tasmota.scale_uint(phase_shift, 0, 255, 0, effective_spatial_period)\n \n # Calculate values for each pixel\n var i = 0\n # Calculate position within the spatial period, including temporal and phase offsets\n var spatial_pos = (temporal_offset + phase_offset) % effective_spatial_period\n\n # Calculate the increment per pixel, in 1/1024 of pixels\n # We calculate 1024*255/effective_spatial_period\n # But for rounding we actually calculate\n # ((1024 * 255 * 2) + 1) / (2 * effective_spatial_period)\n # Note: (1024 * 255 * 2) + 1 = 522241\n var incr_1024 = (522241 / effective_spatial_period) >> 1\n\n # 'spatial_1024' is our accumulator in 1/1024th of pixels, 2^10\n var spatial_1024 = spatial_pos * incr_1024\n var buffer = self.value_buffer._buffer() # 'buffer' is of type 'comptr'\n\n # var effective_spatial_period_1 = effective_spatial_period - 1\n # # Calculate the increment in 1/256 of values\n # var increment = tasmota.scale_uint(effective_spatial_period)\n while i < strip_length\n buffer[i] = spatial_1024 >> 10\n spatial_1024 += incr_1024 # we don't really care about overflow since we clamp modula 255 anyways\n i += 1\n end\n end\n \n # Update animation state based on current time\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Calculate elapsed time since animation started\n var elapsed = time_ms - self.start_time\n \n var strip_length = self.engine.strip_length\n\n # Resize buffer if strip length changed\n if size(self.value_buffer) != strip_length\n self.value_buffer.resize(strip_length)\n end\n \n # Update the value buffer\n self._update_value_buffer(elapsed, strip_length)\n end\n \n # Render the pattern to the provided frame buffer\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Get current parameter values (cached for performance)\n var color_source = self.get_param('color_source') # use get_param to avoid resolving of color_provider\n if color_source == nil\n return false\n end\n \n # Optimization for LUT patterns\n var lut\n if isinstance(color_source, animation.color_provider) && (lut := color_source.get_lut()) != nil\n var lut_factor = color_source.LUT_FACTOR # default = 1, we have only 128 cached values\n var lut_max = 256 >> lut_factor\n var i = 0\n var frame_ptr = frame.pixels._buffer()\n var lut_ptr = lut._buffer()\n var buffer = self.value_buffer._buffer()\n while (i < strip_length)\n var byte_value = buffer[i]\n var lut_index = byte_value >> lut_factor # Divide by 2 using bit shift\n if byte_value == 255\n lut_index = lut_max\n end\n\n var lut_color_ptr = lut_ptr + (lut_index << 2) # calculate the pointer for LUT color\n frame_ptr[0] = lut_color_ptr[0]\n frame_ptr[1] = lut_color_ptr[1]\n frame_ptr[2] = lut_color_ptr[2]\n frame_ptr[3] = lut_color_ptr[3]\n\n # advance to next\n i += 1\n frame_ptr += 4\n end\n else # no LUT, do one color at a time\n # Calculate elapsed time since animation started\n var elapsed = time_ms - self.start_time\n var i = 0\n while (i < strip_length)\n var byte_value = self.value_buffer[i]\n \n # Use the color_source to get color for the byte value (0-255)\n var color = color_source.get_color_for_value(byte_value, elapsed)\n \n frame.set_pixel_color(i, color)\n i += 1\n end\n end\n \n return true\n end\n \n # Handle parameter changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"color_source\"\n # Reinitialize value buffer when color source changes\n self._initialize_value_buffer()\n end\n end\nend\n\nreturn { 'palette_gradient': palette_gradient }"; modules["animations/palette_meter.be"] = "# palette_meter - VU meter style animation with palette gradient colors\n#\n# Displays a gradient-colored bar from the start of the strip up to a level (0-255).\n# Includes optional peak hold indicator that shows the maximum level for a configurable time.\n#\n# Visual representation:\n# level=128 (50%), peak at 200\n# [\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588--------\u2022-------]\n# ^ ^\n# | peak indicator (single pixel)\n# filled gradient area\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass palette_meter : animation.palette_gradient\n # Instance variables for peak tracking\n var peak_level # Current peak level (0-255)\n var peak_time # Time when peak was set (ms)\n var _level # Cached value for 'self.level'\n\n # Parameter definitions - extends palette_gradient params\n static var PARAMS = animation.enc_params({\n # Inherited from palette_gradient: color_source, shift_period, spatial_period, phase_shift\n # New meter-specific parameters\n \"level\": {\"min\": 0, \"max\": 255, \"default\": 255},\n \"peak_hold\": {\"min\": 0, \"default\": 1000} # 0 = disabled, >0 = hold time in ms\n })\n\n # Initialize a new palette_meter\n def init(engine)\n super(self).init(engine)\n\n # Initialize peak tracking\n self.peak_level = 0\n self.peak_time = 0\n self._level = 0\n\n # Override gradient defaults for meter use - static gradient\n self.shift_period = 0\n end\n\n # Override update to handle peak tracking with absolute time\n def update(time_ms)\n var peak_hold = self.peak_hold\n\n if peak_hold > 0\n var level = self.level\n self._level = level # cache value to be used in 'render()'\n var peak_level = self.peak_level\n # Update peak tracking using absolute time\n if level >= peak_level\n # New peak detected, or rearm current peak\n self.peak_level = level\n self.peak_time = time_ms\n elif peak_level > 0\n # Check if peak hold has expired\n var elapsed_since_peak = time_ms - self.peak_time\n if elapsed_since_peak > peak_hold\n # Peak hold expired, reset to current level\n self.peak_level = level\n self.peak_time = time_ms\n end\n end\n end\n\n # Call parent update (computes value_buffer with gradient values)\n super(self).update(time_ms)\n end\n\n # Override render to only display filled pixels and peak indicator\n def render(frame, time_ms, strip_length)\n var color_source = self.get_param('color_source')\n if color_source == nil\n return false\n end\n\n var elapsed = time_ms - self.start_time\n var level = self._level # use cached value in 'update()'\n var peak_hold = self.peak_hold\n\n # Calculate fill position (how many pixels to fill)\n var fill_pixels = tasmota.scale_uint(level, 0, 255, 0, strip_length)\n\n # Calculate peak pixel position\n var peak_pixel = -1\n if peak_hold > 0 && self.peak_level > level\n peak_pixel = tasmota.scale_uint(self.peak_level, 0, 255, 0, strip_length) - 1\n end\n\n\n # Optimization for LUT patterns\n var lut\n if isinstance(color_source, animation.color_provider) && (lut := color_source.get_lut()) != nil\n var lut_factor = color_source.LUT_FACTOR # default = 1, we have only 128 cached values\n var lut_max = 256 >> lut_factor\n var i = 0\n var frame_ptr = frame.pixels._buffer()\n var lut_ptr = lut._buffer()\n var buffer = self.value_buffer._buffer()\n while (i < fill_pixels)\n var byte_value = buffer[i]\n var lut_index = byte_value >> lut_factor # Divide by 2 using bit shift\n if byte_value == 255\n lut_index = lut_max\n end\n\n var lut_color_ptr = lut_ptr + (lut_index << 2) # calculate the pointer for LUT color\n frame_ptr[0] = lut_color_ptr[0]\n frame_ptr[1] = lut_color_ptr[1]\n frame_ptr[2] = lut_color_ptr[2]\n frame_ptr[3] = lut_color_ptr[3]\n\n # advance to next\n i += 1\n frame_ptr += 4\n end\n else\n # Render only filled pixels and peak indicator (leave rest transparent)\n var i = 0\n while i < fill_pixels\n var byte_value = self.value_buffer[i]\n var color = color_source.get_color_for_value(byte_value, elapsed)\n frame.set_pixel_color(i, color)\n # Unfilled pixels stay transparent (not rendered)\n i += 1\n end\n end\n\n # Do we need to show peak pixel?\n if peak_pixel >= fill_pixels\n var byte_value = self.value_buffer[peak_pixel]\n var color = color_source.get_color_for_value(byte_value, elapsed)\n frame.set_pixel_color(peak_pixel, color)\n end\n\n return true\n end\nend\n\nreturn {'palette_meter': palette_meter}\n"; modules["animations/palettes.be"] = "# Palette Examples for Berry Animation Framework\n# This file contains predefined color palettes for use with animations\n# All palettes are in VRGB format: Value, Red, Green, Blue\n\n# Define common palette constants (in VRGB format: Value, Red, Green, Blue)\n# These palettes are compatible with the rich_palette_color\n\n# Standard rainbow palette (7 colors with roughly constant brightness)\nvar PALETTE_RAINBOW = bytes(\n \"FFFC0000\" # Red\n \"FFFF8000\" # Orange\n \"FFFFFF00\" # Yellow\n \"FF00FF00\" # Green\n \"FF00FFFF\" # Cyan\n \"FF0080FF\" # Blue\n \"FF8000FF\" # Violet\n)\n\n# Standard rainbow palette (7 colors with roughly constant brightness) with roll-over\nvar PALETTE_RAINBOW2 = bytes(\n \"FFFC0000\" # Red\n \"FFFF8000\" # Orange\n \"FFFFFF00\" # Yellow\n \"FF00FF00\" # Green\n \"FF00FFFF\" # Cyan\n \"FF0080FF\" # Blue\n \"FF8000FF\" # Violet\n \"FFFC0000\" # Red\n)\n\n# Standard rainbow palette (7 colors + white with roughly constant brightness)\nvar PALETTE_RAINBOW_W = bytes(\n \"FFFC0000\" # Red\n \"FFFF8000\" # Orange\n \"FFFFFF00\" # Yellow\n \"FF00FF00\" # Green\n \"FF00FFFF\" # Cyan\n \"FF0080FF\" # Blue\n \"FF8000FF\" # Violet\n \"FFCCCCCC\" # White\n)\n\n# Standard rainbow palette (7 colors + white with roughly constant brightness) with roll-over\nvar PALETTE_RAINBOW_W2 = bytes(\n \"FFFC0000\" # Red\n \"FFFF8000\" # Orange\n \"FFFFFF00\" # Yellow\n \"FF00FF00\" # Green\n \"FF00FFFF\" # Cyan\n \"FF0080FF\" # Blue\n \"FF8000FF\" # Violet\n \"FFCCCCCC\" # White\n \"FFFC0000\" # Red\n)\n\n# Simple RGB palette (3 colors)\nvar PALETTE_RGB = bytes(\n \"FFFF0000\" # Red (value 0)\n \"FF00FF00\" # Green (value 128)\n \"FF0000FF\" # Blue (value 255)\n)\n\n# Fire effect palette (warm colors)\nvar PALETTE_FIRE = bytes(\n \"FF000000\" # Black (value 0)\n \"FF800000\" # Dark red (value 64)\n \"FFFF0000\" # Red (value 128)\n \"FFFF8000\" # Orange (value 192)\n \"FFFFFF00\" # Yellow (value 255)\n)\n\n# Export all palettes\nreturn {\n \"PALETTE_RAINBOW\": PALETTE_RAINBOW,\n \"PALETTE_RAINBOW2\": PALETTE_RAINBOW2,\n \"PALETTE_RAINBOW_W\": PALETTE_RAINBOW_W,\n \"PALETTE_RAINBOW_W2\": PALETTE_RAINBOW_W2,\n \"PALETTE_RGB\": PALETTE_RGB,\n \"PALETTE_FIRE\": PALETTE_FIRE\n}"; modules["animations/rich_palette.be"] = "# rich_palette - Animation with integrated rich palette color provider\n#\n# This animation class provides direct access to rich palette parameters,\n# forwarding them to an internal rich_palette_colornce.\n# This creates a cleaner API where users can set palette parameters directly\n# on the animation instead of accessing nested color provider properties.\n#\n# Follows the parameterized class specification with parameter forwarding pattern.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass rich_palette : animation.animation\n # Non-parameter instance variables only\n var color_provider # Internal rich_palette_color instance\n \n # Parameter definitions - only rich_palette_color parameters (Animation params inherited)\n static var PARAMS = animation.enc_params({\n # rich_palette_color parameters (forwarded to internal provider)\n \"colors\": {\"type\": \"instance\", \"default\": nil},\n \"period\": {\"min\": 0, \"default\": 5000},\n \"transition_type\": {\"enum\": [1 #-LINEAR-#, 5 #-SINE-#], \"default\": 5 #-SINE-#},\n \"brightness\": {\"min\": 0, \"max\": 255, \"default\": 255}\n })\n \n # Initialize a new rich_palette\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n super(self).init(engine) # Initialize Animation base class\n \n # Create internal rich_palette_color instance\n self.color_provider = animation.rich_palette_color(engine)\n \n # Set the color parameter to our internal provider\n # Use direct values assignment to avoid triggering on_param_changed\n self.values[\"color\"] = self.color_provider\n end\n \n # Handle parameter changes - forward rich palette parameters to internal provider\n #\n # @param name: string - Name of the parameter that changed\n # @param value: any - New value of the parameter\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n # Forward rich palette parameters to internal color provider\n if name == \"colors\" || name == \"period\" || name == \"transition_type\" || \n name == \"brightness\"\n # Set parameter on internal color provider\n self.color_provider.set_param(name, value)\n else\n # Let parent handle animation-specific parameters\n super(self).on_param_changed(name, value)\n end\n end\n \n # Override start to ensure color provider is synchronized\n #\n # @param start_time: int - Optional start time in milliseconds\n # @return self for method chaining\n def start(start_time)\n # Call parent start method\n super(self).start(start_time)\n self.color_provider.start(start_time)\n return self\n end\nend\n\nreturn {'rich_palette': rich_palette}"; modules["animations/solid.be"] = "# Solid Animation Factory\n# Creates a solid color animation using the base Animation class\n# Follows the parameterized class specification with engine-only pattern\n\n# Factory function to create a solid animation\n# Following the \"Engine-only factory functions\" pattern from the specification\n#\n# @param engine: AnimationEngine - Required engine parameter (only parameter)\n# @return Animation - A new solid animation instance with default parameters\ndef solid(engine)\n # Create animation with engine-only constructor\n var anim = animation.animation(engine)\n return anim\nend\n\nreturn {'solid': solid}"; modules["animations/twinkle.be"] = "# Twinkle animation effect for Berry Animation Framework\n#\n# This animation creates a twinkling stars effect with random lights\n# appearing and fading at different positions with customizable density and timing.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass twinkle : animation.animation\n # NO instance variables for parameters - they are handled by the virtual parameter system\n \n # Non-parameter instance variables only\n var current_colors # bytes() buffer storing ARGB colors (4 bytes per pixel)\n var last_update # Last update time for timing\n var random_seed # Seed for random number generation\n \n # Parameter definitions with constraints\n static var PARAMS = animation.enc_params({\n \"color\": {\"default\": 0xFFFFFFBB}, # slightly yellow stars\n \"density\": {\"min\": 0, \"max\": 255, \"default\": 64},\n \"twinkle_speed\": {\"min\": 1, \"max\": 5000, \"default\": 100},\n \"fade_speed\": {\"min\": 0, \"max\": 255, \"default\": 180},\n \"min_brightness\": {\"min\": 0, \"max\": 255, \"default\": 32},\n \"max_brightness\": {\"min\": 0, \"max\": 255, \"default\": 255}\n })\n \n # Initialize a new Twinkle animation\n #\n # @param engine: AnimationEngine - The animation engine (REQUIRED)\n def init(engine)\n # Call parent constructor with engine only\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.current_colors = bytes() # Use bytes() buffer for ARGB colors (4 bytes per pixel)\n self.last_update = 0\n \n # Initialize random seed using engine time\n self.random_seed = self.engine.time_ms % 65536\n \n # Initialize buffer based on strip length from engine\n self._initialize_arrays()\n end\n \n # Initialize buffer based on current strip length\n def _initialize_arrays()\n var strip_length = self.engine.strip_length\n \n # Create new bytes() buffer for colors (4 bytes per pixel: ARGB)\n # Alpha channel serves as the active state: alpha=0 means off, alpha>0 means active\n self.current_colors.clear()\n self.current_colors.resize(strip_length * 4)\n \n # Initialize all pixels to off state (transparent = alpha 0)\n var i = 0\n while i < strip_length\n self.current_colors.set(i * 4, 0x00000000, -4) # Transparent (alpha = 0)\n i += 1\n end\n end\n \n # Handle parameter changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"twinkle_speed\"\n # Handle twinkle_speed - can be Hz (1-20) or period in ms (50-5000)\n if value >= 50 # Assume it's period in milliseconds\n # Convert period (ms) to frequency (Hz): Hz = 1000 / ms\n # Clamp to reasonable range 1-20 Hz\n var hz = 1000 / value\n if hz < 1\n hz = 1\n elif hz > 20\n hz = 20\n end\n # Update the parameter with the converted value\n self.set_param(\"twinkle_speed\", hz)\n end\n end\n end\n \n # Simple pseudo-random number generator\n # Uses a linear congruential generator for consistent results\n def _random()\n self.random_seed = (self.random_seed * 1103515245 + 12345) & 0x7FFFFFFF\n return self.random_seed\n end\n \n # Get random number in range [0, max)\n def _random_range(max)\n if max <= 0\n return 0\n end\n return self._random() % max\n end\n \n # Update animation state based on current time\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Access parameters via virtual members\n var twinkle_speed = self.twinkle_speed\n \n # Check if it's time to update the twinkle simulation\n # Update frequency is based on twinkle_speed (Hz)\n var update_interval = 1000 / twinkle_speed # milliseconds between updates\n if time_ms - self.last_update >= update_interval\n self.last_update = time_ms\n self._update_twinkle_simulation(time_ms)\n end\n end\n \n # Update the twinkle simulation with alpha-based fading\n def _update_twinkle_simulation(time_ms)\n # Access parameters via virtual members (cache for performance)\n var fade_speed = self.fade_speed\n var density = self.density\n var min_brightness = self.min_brightness\n var max_brightness = self.max_brightness\n var color = self.color\n \n var strip_length = self.engine.strip_length\n \n # Ensure buffer is properly sized\n if self.current_colors.size() != strip_length * 4\n self._initialize_arrays()\n end\n \n # Step 1: Fade existing twinkles by reducing alpha\n var i = 0\n while i < strip_length\n var current_color = self.current_colors.get(i * 4, -4)\n var alpha = (current_color >> 24) & 0xFF\n \n if alpha > 0\n # Calculate fade amount based on fade_speed\n var fade_amount = tasmota.scale_uint(fade_speed, 0, 255, 1, 20)\n if alpha <= fade_amount\n # Star has faded completely - reset to transparent\n self.current_colors.set(i * 4, 0x00000000, -4)\n else\n # Reduce alpha while keeping RGB components unchanged\n var new_alpha = alpha - fade_amount\n var rgb = current_color & 0x00FFFFFF # Keep RGB, clear alpha\n self.current_colors.set(i * 4, (new_alpha << 24) | rgb, -4)\n end\n end\n i += 1\n end\n \n # Step 2: Randomly create new twinkles based on density\n # For each pixel, check if it should twinkle based on density probability\n var j = 0\n while j < strip_length\n # Only consider pixels that are currently off (alpha = 0)\n var current_color = self.current_colors.get(j * 4, -4)\n var alpha = (current_color >> 24) & 0xFF\n \n if alpha == 0\n # Use density as probability out of 255\n if self._random_range(255) < density\n # Create new star at full brightness with random intensity alpha\n var star_alpha = min_brightness + self._random_range(max_brightness - min_brightness + 1)\n \n # Get base color (automatically resolves value_providers)\n var base_color = color\n \n # Extract RGB components (ignore original alpha)\n var r = (base_color >> 16) & 0xFF\n var g = (base_color >> 8) & 0xFF\n var b = base_color & 0xFF\n \n # Create new star with full-brightness color and variable alpha\n self.current_colors.set(j * 4, (star_alpha << 24) | (r << 16) | (g << 8) | b, -4)\n end\n end\n j += 1\n end\n end\n \n # Render the twinkle to the provided frame buffer\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Ensure buffer is properly sized\n if self.current_colors.size() != strip_length * 4\n self._initialize_arrays()\n end\n \n # Only render pixels that are actually twinkling (non-transparent)\n var modified = false\n var i = 0\n while i < strip_length\n if i < frame.width\n var color = self.current_colors.get(i * 4, -4)\n # Only set pixels that have some alpha (are visible)\n if (color >> 24) & 0xFF > 0\n frame.set_pixel_color(i, color)\n modified = true\n end\n end\n i += 1\n end\n \n return modified\n end\nend\n\nreturn { 'twinkle': twinkle }"; modules["animations_future/bounce.be"] = "# Bounce animation effect for Berry Animation Framework\n#\n# This animation creates bouncing effects where patterns bounce back and forth\n# across the LED strip with configurable physics and damping.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass BounceAnimation : animation.animation\n # Non-parameter instance variables only\n var current_position # Current position in 1/256th pixels\n var current_velocity # Current velocity in 1/256th pixels per second\n var bounce_center # Center point for bouncing\n var source_frame # Frame buffer for source animation\n var current_colors # Array of current colors for each pixel\n var last_update_time # Last update time for physics calculation\n \n # Parameter definitions following parameterized class specification\n static var PARAMS = animation.enc_params({\n \"source_animation\": {\"type\": \"instance\", \"default\": nil},\n \"bounce_speed\": {\"min\": 0, \"max\": 255, \"default\": 128},\n \"bounce_range\": {\"min\": 0, \"max\": 1000, \"default\": 0},\n \"damping\": {\"min\": 0, \"max\": 255, \"default\": 250},\n \"gravity\": {\"min\": 0, \"max\": 255, \"default\": 0}\n })\n \n # Initialize a new Bounce animation\n def init(engine)\n # Call parent constructor with engine only\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.current_position = 0\n self.current_velocity = 0\n self.bounce_center = 0\n self.source_frame = nil\n self.current_colors = []\n self.last_update_time = 0\n \n # Initialize with default strip length\n self._initialize_buffers()\n end\n \n # Initialize frame buffers and arrays\n def _initialize_buffers()\n var current_strip_length = self.engine.strip_length\n self.bounce_center = current_strip_length * 256 / 2 # Center in 1/256th pixels\n self.current_position = self.bounce_center\n \n # Initialize velocity based on bounce_speed\n var pixels_per_second = tasmota.scale_uint(self.bounce_speed, 0, 255, 0, 20)\n self.current_velocity = pixels_per_second * 256 # Convert to 1/256th pixels per second\n \n # Initialize rendering buffers\n self.source_frame = animation.frame_buffer(current_strip_length)\n self.current_colors.resize(current_strip_length)\n \n # Initialize colors to black\n var i = 0\n while i < current_strip_length\n self.current_colors[i] = 0xFF000000\n i += 1\n end\n end\n \n # Override start method for timing control and value_provider propagation\n def start(time_ms)\n # Call parent start first (handles value_provider propagation)\n super(self).start(time_ms)\n \n # Reset physics state for fresh start/restart\n var actual_start_time = time_ms != nil ? time_ms : self.engine.time_ms\n self.last_update_time = actual_start_time\n \n # Reset position and velocity\n self._initialize_buffers()\n \n # Start source animation if it exists\n var current_source = self.source_animation\n if current_source != nil\n current_source.start(actual_start_time)\n end\n \n return self\n end\n \n # Handle parameter changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"bounce_speed\"\n # Update velocity if speed changed\n var pixels_per_second = tasmota.scale_uint(value, 0, 255, 0, 20)\n var new_velocity = pixels_per_second * 256\n # Preserve direction\n if self.current_velocity < 0\n self.current_velocity = -new_velocity\n else\n self.current_velocity = new_velocity\n end\n end\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n \n # Initialize last_update_time on first update\n if self.last_update_time == 0\n self.last_update_time = time_ms\n end\n \n # Calculate time delta\n var dt = time_ms - self.last_update_time\n if dt <= 0\n return\n end\n self.last_update_time = time_ms\n \n # Update physics\n self._update_physics(dt)\n \n # Update source animation if it exists\n var current_source = self.source_animation\n if current_source != nil\n if !current_source.is_running\n current_source.start(self.start_time)\n end\n current_source.update(time_ms)\n end\n \n # Calculate bounced colors\n self._calculate_bounce()\n end\n \n # Update bounce physics\n def _update_physics(dt_ms)\n # Cache parameter values for performance\n var current_gravity = self.gravity\n var current_bounce_range = self.bounce_range\n var current_strip_length = self.engine.strip_length\n var current_damping = self.damping\n \n # Use integer arithmetic for physics (dt in milliseconds)\n \n # Apply gravity (downward acceleration)\n if current_gravity > 0\n var gravity_accel = tasmota.scale_uint(current_gravity, 0, 255, 0, 1000) # pixels/sec\u00b2\n # Convert to 1/256th pixels per millisecond: accel * dt / 1000\n var velocity_change = gravity_accel * dt_ms / 1000\n self.current_velocity += velocity_change\n end\n \n # Update position: velocity is in 1/256th pixels per second\n # Convert to position change: velocity * dt / 1000\n self.current_position += self.current_velocity * dt_ms / 1000\n \n # Calculate bounce boundaries\n var effective_range = current_bounce_range > 0 ? current_bounce_range : current_strip_length\n var half_range = effective_range * 256 / 2\n var min_pos = self.bounce_center - half_range\n var max_pos = self.bounce_center + half_range\n \n # Check for bounces\n var bounced = false\n if self.current_position <= min_pos\n self.current_position = min_pos\n self.current_velocity = -self.current_velocity\n bounced = true\n elif self.current_position >= max_pos\n self.current_position = max_pos\n self.current_velocity = -self.current_velocity\n bounced = true\n end\n \n # Apply damping on bounce\n if bounced && current_damping < 255\n var damping_factor = tasmota.scale_uint(current_damping, 0, 255, 0, 255)\n self.current_velocity = tasmota.scale_uint(self.current_velocity, 0, 255, 0, damping_factor)\n if self.current_velocity < 0\n self.current_velocity = -tasmota.scale_uint(-self.current_velocity, 0, 255, 0, damping_factor)\n end\n end\n end\n \n # Calculate bounced colors for all pixels\n def _calculate_bounce()\n # Clear source frame\n self.source_frame.clear()\n \n # Render source animation to frame\n var current_source = self.source_animation\n if current_source != nil\n current_source.render(self.source_frame, 0)\n end\n \n # Cache strip length for performance\n var current_strip_length = self.engine.strip_length\n \n # Apply bounce transformation\n var pixel_position = self.current_position / 256 # Convert to pixel units\n var offset = pixel_position - current_strip_length / 2 # Offset from center\n \n var i = 0\n while i < current_strip_length\n var source_pos = i - offset\n \n # Clamp to strip bounds\n if source_pos >= 0 && source_pos < current_strip_length\n self.current_colors[i] = self.source_frame.get_pixel_color(source_pos)\n else\n self.current_colors[i] = 0xFF000000 # Black for out-of-bounds\n end\n \n i += 1\n end\n end\n \n # Render bounce to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\n# Factory functions following parameterized class specification\n\n# Create a basic bounce animation\n#\n# @param engine: AnimationEngine - Animation engine instance\n# @return BounceAnimation - A new bounce animation instance\ndef bounce_basic(engine)\n var bounce = animation.bounce_animation(engine)\n bounce.bounce_speed = 128\n bounce.bounce_range = 0 # full strip range\n bounce.damping = 250\n bounce.gravity = 0\n return bounce\nend\n\n# Create a gravity bounce animation\n#\n# @param engine: AnimationEngine - Animation engine instance\n# @return BounceAnimation - A new bounce animation instance\ndef bounce_gravity(engine)\n var bounce = animation.bounce_animation(engine)\n bounce.bounce_speed = 100\n bounce.bounce_range = 0 # full strip range\n bounce.damping = 240\n bounce.gravity = 128\n return bounce\nend\n\n# Create a constrained bounce animation\n#\n# @param engine: AnimationEngine - Animation engine instance\n# @return BounceAnimation - A new bounce animation instance\ndef bounce_constrained(engine)\n var bounce = animation.bounce_animation(engine)\n bounce.bounce_speed = 150\n bounce.bounce_range = 15 # constrained range\n bounce.damping = 250\n bounce.gravity = 0\n return bounce\nend\n\nreturn {'bounce_animation': BounceAnimation, 'bounce_basic': bounce_basic, 'bounce_gravity': bounce_gravity, 'bounce_constrained': bounce_constrained}"; modules["animations_future/fire.be"] = "# Fire animation effect for Berry Animation Framework\n#\n# This animation creates a realistic fire effect with flickering flames.\n# The fire uses random intensity variations and warm colors to simulate flames.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass fire : animation.animation\n # Non-parameter instance variables only\n var heat_map # bytes() buffer storing heat values for each pixel (0-255)\n var current_colors # bytes() buffer storing ARGB colors (4 bytes per pixel)\n var last_update # Last update time for flicker timing\n var random_seed # Seed for random number generation\n \n # Parameter definitions following parameterized class specification\n static var PARAMS = animation.enc_params({\n # 'color' for the comet head (32-bit ARGB value), inherited from animation class\n \"intensity\": {\"min\": 0, \"max\": 255, \"default\": 180},\n \"flicker_speed\": {\"min\": 1, \"max\": 20, \"default\": 8},\n \"flicker_amount\": {\"min\": 0, \"max\": 255, \"default\": 100},\n \"cooling_rate\": {\"min\": 0, \"max\": 255, \"default\": 55},\n \"sparking_rate\": {\"min\": 0, \"max\": 255, \"default\": 120}\n })\n \n # Initialize a new Fire animation\n #\n # @param engine: AnimationEngine - The animation engine (required)\n def init(engine)\n log(\"ANI: `fire` animation is still in alpha and will be refactored\")\n # Call parent constructor with engine\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.heat_map = bytes() # Use bytes() buffer for efficient 0-255 value storage\n self.current_colors = bytes() # Use bytes() buffer for ARGB colors (4 bytes per pixel)\n self.last_update = 0\n \n # Initialize random seed using engine time\n self.random_seed = self.engine.time_ms % 65536\n end\n \n # Initialize buffers based on current strip length\n def _initialize_buffers()\n var strip_length = self.engine.strip_length\n \n # Create new bytes() buffer for heat values (1 byte per pixel)\n self.heat_map.clear()\n self.heat_map.resize(strip_length)\n \n # Create new bytes() buffer for colors (4 bytes per pixel: ARGB)\n self.current_colors.clear()\n self.current_colors.resize(strip_length * 4)\n \n # Initialize all pixels to zero heat and black color (0xFF000000)\n var i = 0\n while i < strip_length\n self.current_colors.set(i * 4, 0xFF000000, -4) # Black with full alpha\n i += 1\n end\n end\n \n # Simple pseudo-random number generator\n # Uses a linear congruential generator for consistent results\n def _random()\n self.random_seed = (self.random_seed * 1103515245 + 12345) & 0x7FFFFFFF\n return self.random_seed\n end\n \n # Get random number in range [0, max)\n def _random_range(max)\n if max <= 0\n return 0\n end\n return self._random() % max\n end\n \n # Update animation state based on current time\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Check if it's time to update the fire simulation\n # Update frequency is based on flicker_speed (Hz)\n var flicker_speed = self.flicker_speed # Cache parameter value\n var update_interval = 1000 / flicker_speed # milliseconds between updates\n if time_ms - self.last_update >= update_interval\n self.last_update = time_ms\n self._update_fire_simulation(time_ms)\n end\n end\n \n # Update the fire simulation\n def _update_fire_simulation(time_ms)\n # Cache parameter values for performance\n var cooling_rate = self.cooling_rate\n var sparking_rate = self.sparking_rate\n var intensity = self.intensity\n var flicker_amount = self.flicker_amount\n var color_param = self.color\n var strip_length = self.engine.strip_length\n \n # Ensure buffers are correct size (bytes() uses .size() method)\n if self.heat_map.size() != strip_length || self.current_colors.size() != strip_length * 4\n self._initialize_buffers()\n end\n \n # Step 1: Cool down every pixel a little\n var i = 0\n while i < strip_length\n var cooldown = self._random_range(tasmota.scale_uint(cooling_rate, 0, 255, 0, 10) + 2)\n if cooldown >= self.heat_map[i]\n self.heat_map[i] = 0\n else\n self.heat_map[i] -= cooldown\n end\n i += 1\n end\n \n # Step 2: Heat from each pixel drifts 'up' and diffuses a little\n # Only do this if we have at least 3 pixels\n if strip_length >= 3\n var k = strip_length - 1\n while k >= 2\n var heat_avg = (self.heat_map[k-1] + self.heat_map[k-2] + self.heat_map[k-2]) / 3\n # Ensure the result is an integer in valid range (0-255)\n if heat_avg < 0\n heat_avg = 0\n elif heat_avg > 255\n heat_avg = 255\n end\n self.heat_map[k] = int(heat_avg)\n k -= 1\n end\n end\n \n # Step 3: Randomly ignite new 'sparks' of heat near the bottom\n if self._random_range(255) < sparking_rate\n var spark_pos = self._random_range(7) # Sparks only in bottom 7 pixels\n var spark_heat = self._random_range(95) + 160 # Heat between 160-254\n # Ensure spark heat is in valid range (should already be, but be explicit)\n if spark_heat > 255\n spark_heat = 255\n end\n if spark_pos < strip_length\n self.heat_map[spark_pos] = spark_heat\n end\n end\n \n # Step 4: Convert heat to colors\n i = 0\n while i < strip_length\n var heat = self.heat_map[i]\n \n # Apply base intensity scaling\n heat = tasmota.scale_uint(heat, 0, 255, 0, intensity)\n \n # Add flicker effect\n if flicker_amount > 0\n var flicker = self._random_range(flicker_amount)\n # Randomly add or subtract flicker\n if self._random_range(2) == 0\n heat = heat + flicker\n else\n if heat > flicker\n heat = heat - flicker\n else\n heat = 0\n end\n end\n \n # Clamp to valid range\n if heat > 255\n heat = 255\n end\n end\n \n # Get color from provider based on heat value\n var color = 0xFF000000 # Default to black\n if heat > 0\n # Get the color parameter (may be nil for default)\n var resolved_color = color_param\n \n # If color is nil, create default fire palette\n if resolved_color == nil\n # Create default fire palette on demand\n var fire_provider = animation.rich_palette_color(self.engine)\n fire_provider.colors = animation.PALETTE_FIRE\n fire_provider.period = 0 # Use value-based color mapping, not time-based\n fire_provider.transition_type = 1 # Use sine transition (smooth)\n fire_provider.brightness = 255\n resolved_color = fire_provider\n end\n \n # If the color is a provider that supports get_color_for_value, use it\n if animation.is_color_provider(resolved_color) && resolved_color.get_color_for_value != nil\n # Use value-based color mapping for heat\n color = resolved_color.get_color_for_value(heat, 0)\n else\n # Use the resolved color and apply heat as brightness scaling\n color = resolved_color\n \n # Apply heat as brightness scaling\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n r = tasmota.scale_uint(heat, 0, 255, 0, r)\n g = tasmota.scale_uint(heat, 0, 255, 0, g)\n b = tasmota.scale_uint(heat, 0, 255, 0, b)\n \n color = (a << 24) | (r << 16) | (g << 8) | b\n end\n end\n \n self.current_colors.set(i * 4, color, -4)\n i += 1\n end\n end\n \n # Render the fire to the provided frame buffer\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Render each pixel with its current color\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors.get(i * 4, -4))\n end\n i += 1\n end\n \n return true\n end\n \n # Override start method for timing control\n def start(time_ms)\n # Call parent start first\n super(self).start(time_ms)\n \n # Reset timing and reinitialize buffers\n self.last_update = 0\n self._initialize_buffers()\n \n # Reset random seed\n self.random_seed = self.engine.time_ms % 65536\n \n return self\n end\nend\n\nreturn {'fire': fire}"; modules["animations_future/jitter.be"] = "# Jitter animation effect for Berry Animation Framework\n#\n# This animation adds random jitter/shake effects to patterns with configurable\n# intensity, frequency, and jitter types (position, color, brightness).\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass JitterAnimation : animation.animation\n # Non-parameter instance variables only\n var random_seed # Seed for random number generation\n var last_jitter_time # Last time jitter was updated\n var jitter_offsets # Array of current jitter offsets per pixel\n var source_frame # Frame buffer for source animation\n var current_colors # Array of current colors for each pixel\n \n # Parameter definitions\n static var PARAMS = animation.enc_params({\n \"source_animation\": {\"type\": \"instance\", \"default\": nil},\n \"jitter_intensity\": {\"min\": 0, \"max\": 255, \"default\": 100},\n \"jitter_frequency\": {\"min\": 0, \"max\": 255, \"default\": 60},\n \"jitter_type\": {\"min\": 0, \"max\": 3, \"default\": 0},\n \"position_range\": {\"min\": 0, \"max\": 255, \"default\": 50},\n \"color_range\": {\"min\": 0, \"max\": 255, \"default\": 30},\n \"brightness_range\": {\"min\": 0, \"max\": 255, \"default\": 40}\n })\n \n # Initialize a new Jitter animation\n def init(engine)\n # Call parent constructor with engine\n super(self).init(engine)\n \n # Initialize random seed using engine time\n self.random_seed = self.engine.time_ms % 65536\n \n # Initialize state\n self.last_jitter_time = 0\n \n # Initialize buffers\n self._initialize_buffers()\n end\n \n # Initialize buffers based on current strip length\n def _initialize_buffers()\n var current_strip_length = self.engine.strip_length\n self.jitter_offsets = []\n self.jitter_offsets.resize(current_strip_length)\n self.source_frame = animation.frame_buffer(current_strip_length)\n self.current_colors = []\n self.current_colors.resize(current_strip_length)\n \n # Initialize arrays\n var i = 0\n while i < current_strip_length\n self.jitter_offsets[i] = 0\n self.current_colors[i] = 0xFF000000\n i += 1\n end\n end\n \n # Override start method for lifecycle control\n def start(time_ms)\n # Call parent start first (handles value_provider propagation)\n super(self).start(time_ms)\n \n # Reset jitter timing\n self.last_jitter_time = time_ms != nil ? time_ms : self.engine.time_ms\n \n # Reinitialize buffers in case strip length changed\n self._initialize_buffers()\n \n return self\n end\n \n # Simple pseudo-random number generator\n def _random()\n self.random_seed = (self.random_seed * 1103515245 + 12345) & 0x7FFFFFFF\n return self.random_seed\n end\n \n # Get random number in range [-max_range, max_range]\n def _random_range(max_range)\n if max_range <= 0\n return 0\n end\n var val = self._random() % (max_range * 2 + 1)\n return val - max_range\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n\n # Cache parameter values for performance\n var jitter_frequency = self.jitter_frequency\n var source_animation = self.source_animation\n \n # Update jitter at specified frequency\n if jitter_frequency > 0\n # Frequency: 0-255 maps to 0-30 Hz\n var hz = tasmota.scale_uint(jitter_frequency, 0, 255, 0, 30)\n var interval = hz > 0 ? 1000 / hz : 1000\n \n if time_ms - self.last_jitter_time >= interval\n self.last_jitter_time = time_ms\n self._update_jitter()\n end\n end\n \n # Update source animation if it exists\n if source_animation != nil\n source_animation.update(time_ms)\n end\n \n # Calculate jittered colors\n self._calculate_jitter()\n end\n \n # Update jitter offsets\n def _update_jitter()\n var current_strip_length = self.engine.strip_length\n var jitter_intensity = self.jitter_intensity\n var max_offset = tasmota.scale_uint(jitter_intensity, 0, 255, 0, 10)\n \n var i = 0\n while i < current_strip_length\n # Generate new random offset based on intensity\n self.jitter_offsets[i] = self._random_range(max_offset)\n i += 1\n end\n end\n \n # Calculate jittered colors for all pixels\n def _calculate_jitter()\n var current_strip_length = self.engine.strip_length\n var source_animation = self.source_animation\n var jitter_type = self.jitter_type\n var position_range = self.position_range\n \n # Clear source frame\n self.source_frame.clear()\n \n # Render source animation to frame\n if source_animation != nil\n source_animation.render(self.source_frame, 0)\n end\n \n # Apply jitter transformation\n var i = 0\n while i < current_strip_length\n var base_color = 0xFF000000\n \n if jitter_type == 0 || jitter_type == 3\n # Position jitter\n var jitter_pixels = tasmota.scale_uint(self.jitter_offsets[i], -10, 10, -position_range / 10, position_range / 10)\n var source_pos = i + jitter_pixels\n \n # Clamp to strip bounds\n if source_pos >= 0 && source_pos < current_strip_length\n base_color = self.source_frame.get_pixel_color(source_pos)\n else\n base_color = 0xFF000000\n end\n else\n # No position jitter, use original position\n base_color = self.source_frame.get_pixel_color(i)\n end\n \n # Apply color and brightness jitter\n if (jitter_type == 1 || jitter_type == 2 || jitter_type == 3) && base_color != 0xFF000000\n base_color = self._apply_color_jitter(base_color, i)\n end\n \n self.current_colors[i] = base_color\n i += 1\n end\n end\n \n # Apply color/brightness jitter to a color\n def _apply_color_jitter(color, pixel_index)\n # Cache parameter values for performance\n var jitter_type = self.jitter_type\n var color_range = self.color_range\n var brightness_range = self.brightness_range\n \n # Extract ARGB components\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n if jitter_type == 1 || jitter_type == 3\n # Color jitter - add random values to RGB\n var color_jitter = tasmota.scale_uint(color_range, 0, 255, 0, 30)\n r += self._random_range(color_jitter)\n g += self._random_range(color_jitter)\n b += self._random_range(color_jitter)\n end\n \n if jitter_type == 2 || jitter_type == 3\n # Brightness jitter - scale all RGB components\n var brightness_jitter = tasmota.scale_uint(brightness_range, 0, 255, 0, 50)\n var brightness_factor = 128 + self._random_range(brightness_jitter)\n if brightness_factor < 0\n brightness_factor = 0\n elif brightness_factor > 255\n brightness_factor = 255\n end\n \n r = tasmota.scale_uint(r, 0, 255, 0, brightness_factor)\n g = tasmota.scale_uint(g, 0, 255, 0, brightness_factor)\n b = tasmota.scale_uint(b, 0, 255, 0, brightness_factor)\n end\n \n # Clamp components to valid range\n if r > 255\n r = 255\n elif r < 0\n r = 0\n end\n if g > 255\n g = 255\n elif g < 0\n g = 0\n end\n if b > 255\n b = 255\n elif b < 0\n b = 0\n end\n \n return (a << 24) | (r << 16) | (g << 8) | b\n end\n \n # Render jitter to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\n# Factory functions for common jitter presets\n\n# Create a position jitter animation\ndef jitter_position(engine)\n var anim = animation.jitter_animation(engine)\n anim.jitter_type = 0\n anim.position_range = 50\n return anim\nend\n\n# Create a color jitter animation\ndef jitter_color(engine)\n var anim = animation.jitter_animation(engine)\n anim.jitter_type = 1\n anim.color_range = 30\n return anim\nend\n\n# Create a brightness jitter animation\ndef jitter_brightness(engine)\n var anim = animation.jitter_animation(engine)\n anim.jitter_type = 2\n anim.brightness_range = 40\n return anim\nend\n\n# Create a full jitter animation (all types)\ndef jitter_all(engine)\n var anim = animation.jitter_animation(engine)\n anim.jitter_type = 3\n anim.position_range = 50\n anim.color_range = 30\n anim.brightness_range = 40\n return anim\nend\n\nreturn {\n 'jitter_animation': JitterAnimation,\n 'jitter_position': jitter_position,\n 'jitter_color': jitter_color,\n 'jitter_brightness': jitter_brightness,\n 'jitter_all': jitter_all\n}"; modules["animations_future/plasma.be"] = "# Plasma animation effect for Berry Animation Framework\n#\n# This animation creates classic plasma effects using sine wave interference\n# patterns with configurable frequencies, phases, and time-based animation.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass PlasmaAnimation : animation.animation\n # Non-parameter instance variables only\n var current_colors # Array of current colors for each pixel\n var time_phase # Current time-based phase\n \n # Parameter definitions following parameterized class specification\n static var PARAMS = animation.enc_params({\n \"color\": {\"default\": nil},\n \"freq_x\": {\"min\": 1, \"max\": 255, \"default\": 32},\n \"freq_y\": {\"min\": 1, \"max\": 255, \"default\": 23},\n \"phase_x\": {\"min\": 0, \"max\": 255, \"default\": 0},\n \"phase_y\": {\"min\": 0, \"max\": 255, \"default\": 64},\n \"time_speed\": {\"min\": 0, \"max\": 255, \"default\": 50},\n \"blend_mode\": {\"min\": 0, \"max\": 2, \"default\": 0}\n })\n \n # Initialize a new Plasma animation\n #\n # @param engine: AnimationEngine - Required animation engine reference\n def init(engine)\n # Call parent constructor with engine\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.time_phase = 0\n \n # Initialize current_colors array - will be resized when strip length is known\n self.current_colors = []\n self._initialize_colors()\n end\n \n # Fast sine calculation using Tasmota's optimized sine function\n # Input: angle in 0-255 range (mapped to 0-2\u03c0)\n # Output: sine value in 0-255 range (mapped from -1 to 1)\n def _sine(angle)\n # Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)\n var tasmota_angle = tasmota.scale_uint(angle, 0, 255, 0, 32767)\n \n # Get sine value from -4096 to 4096 (representing -1.0 to 1.0)\n var sine_val = tasmota.sine_int(tasmota_angle)\n \n # Map from -4096..4096 to 0..255 for plasma calculations\n return tasmota.scale_uint(sine_val, -4096, 4096, 0, 255)\n end\n \n # Initialize colors array based on current strip length\n def _initialize_colors()\n var strip_length = self.engine.strip_length\n self.current_colors.resize(strip_length)\n var i = 0\n while i < strip_length\n self.current_colors[i] = 0xFF000000\n i += 1\n end\n end\n \n # Start/restart the animation\n def start(time_ms)\n # Call parent start first\n super(self).start(time_ms)\n \n # Initialize default color if not set\n if self.color == nil\n var rainbow_provider = animation.rich_palette_color(self.engine)\n rainbow_provider.colors = animation.PALETTE_RAINBOW\n rainbow_provider.period = 5000\n rainbow_provider.transition_type = 1\n rainbow_provider.brightness = 255\n self.color = rainbow_provider\n end\n \n # Reset time phase\n self.time_phase = 0\n \n return self\n end\n \n # Handle parameter changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"color\" && value == nil\n # Reset to default rainbow palette when color is set to nil\n var rainbow_provider = animation.rich_palette_color(self.engine)\n rainbow_provider.colors = animation.PALETTE_RAINBOW\n rainbow_provider.period = 5000\n rainbow_provider.transition_type = 1\n rainbow_provider.brightness = 255\n # Set the parameter directly to avoid recursion\n self.set_param(\"color\", rainbow_provider)\n end\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n \n # Update time phase based on speed\n var current_time_speed = self.time_speed\n if current_time_speed > 0\n var elapsed = time_ms - self.start_time\n # Speed: 0-255 maps to 0-8 cycles per second\n var cycles_per_second = tasmota.scale_uint(current_time_speed, 0, 255, 0, 8)\n if cycles_per_second > 0\n self.time_phase = (elapsed * cycles_per_second / 1000) % 256\n end\n end\n \n # Calculate plasma colors\n self._calculate_plasma(time_ms)\n end\n \n # Calculate plasma colors for all pixels\n def _calculate_plasma(time_ms)\n var strip_length = self.engine.strip_length\n \n # Ensure colors array is properly sized\n if size(self.current_colors) != strip_length\n self._initialize_colors()\n end\n \n # Cache parameter values for performance\n var current_freq_x = self.freq_x\n var current_freq_y = self.freq_y\n var current_phase_x = self.phase_x\n var current_phase_y = self.phase_y\n var current_blend_mode = self.blend_mode\n var current_color = self.color\n \n var i = 0\n while i < strip_length\n # Map pixel position to 0-255 range\n var x = tasmota.scale_uint(i, 0, strip_length - 1, 0, 255)\n \n # Calculate plasma components\n var comp1 = self._sine((x * current_freq_x / 32) + current_phase_x + self.time_phase)\n var comp2 = self._sine((x * current_freq_y / 32) + current_phase_y + (self.time_phase * 2))\n \n # Blend components based on blend mode\n var plasma_value = 0\n if current_blend_mode == 0\n # Add mode\n plasma_value = (comp1 + comp2) / 2\n elif current_blend_mode == 1\n # Multiply mode\n plasma_value = tasmota.scale_uint(comp1, 0, 255, 0, comp2)\n else\n # Average mode (default)\n plasma_value = (comp1 + comp2) / 2\n end\n \n # Ensure value is in valid range\n if plasma_value > 255\n plasma_value = 255\n elif plasma_value < 0\n plasma_value = 0\n end\n \n # Get color from provider\n var color = 0xFF000000\n \n # If the color is a provider that supports get_color_for_value, use it\n if animation.is_color_provider(current_color) && current_color.get_color_for_value != nil\n color = current_color.get_color_for_value(plasma_value, 0)\n else\n # Use resolve_value with plasma influence\n color = self.resolve_value(current_color, \"color\", time_ms + plasma_value * 10)\n end\n \n self.current_colors[i] = color\n i += 1\n end\n end\n \n # Render plasma to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\n# Factory functions\n\n# Create a classic rainbow plasma animation\n#\n# @param engine: AnimationEngine - Required animation engine reference\n# @return PlasmaAnimation - A new plasma animation instance with rainbow colors\ndef plasma_rainbow(engine)\n var anim = animation.plasma_animation(engine)\n # Use default rainbow color (nil triggers rainbow in on_param_changed)\n anim.color = nil\n anim.time_speed = 50\n return anim\nend\n\n# Create a fast plasma animation\n#\n# @param engine: AnimationEngine - Required animation engine reference\n# @return PlasmaAnimation - A new fast plasma animation instance\ndef plasma_fast(engine)\n var anim = animation.plasma_animation(engine)\n anim.color = nil # Default rainbow\n anim.time_speed = 150\n anim.freq_x = 48\n anim.freq_y = 35\n return anim\nend\n\nreturn {'plasma_animation': PlasmaAnimation, 'plasma_rainbow': plasma_rainbow, 'plasma_fast': plasma_fast}"; modules["animations_future/scale.be"] = "# Scale animation effect for Berry Animation Framework\n#\n# This animation scales patterns up or down with configurable scaling factors,\n# interpolation methods, and center points.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass ScaleAnimation : animation.animation\n # Non-parameter instance variables only\n var scale_phase # Current phase for animated scaling\n var source_frame # Frame buffer for source animation\n var current_colors # Array of current colors for each pixel\n var start_time # Animation start time\n \n # Parameter definitions following parameterized class specification\n static var PARAMS = animation.enc_params({\n \"source_animation\": {\"type\": \"instance\", \"default\": nil},\n \"scale_factor\": {\"min\": 1, \"max\": 255, \"default\": 128},\n \"scale_speed\": {\"min\": 0, \"max\": 255, \"default\": 0},\n \"scale_mode\": {\"min\": 0, \"max\": 3, \"default\": 0},\n \"scale_center\": {\"min\": 0, \"max\": 255, \"default\": 128},\n \"interpolation\": {\"min\": 0, \"max\": 1, \"default\": 1}\n })\n \n # Initialize a new Scale animation\n # @param engine: AnimationEngine - Required animation engine\n def init(engine)\n # Call parent constructor with engine\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.scale_phase = 0\n self.start_time = self.engine.time_ms\n self._initialize_buffers()\n end\n \n # Initialize frame buffers based on current strip length\n def _initialize_buffers()\n var current_strip_length = self.engine.strip_length\n self.source_frame = animation.frame_buffer(current_strip_length)\n self.current_colors = []\n self.current_colors.resize(current_strip_length)\n \n # Initialize colors to black\n var i = 0\n while i < current_strip_length\n self.current_colors[i] = 0xFF000000\n i += 1\n end\n end\n \n # Start/restart the animation\n def start(time_ms)\n # Call parent start first (handles value_provider propagation)\n super(self).start(time_ms)\n \n # Reset scale phase for animated modes\n self.scale_phase = 0\n \n # Initialize timing\n if time_ms == nil\n time_ms = self.engine.time_ms\n end\n self.start_time = time_ms\n \n return self\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n\n # Cache parameter values for performance\n var current_scale_speed = self.scale_speed\n var current_scale_mode = self.scale_mode\n var current_source_animation = self.source_animation\n \n # Update scale phase for animated modes\n if current_scale_speed > 0 && current_scale_mode > 0\n var elapsed = time_ms - self.start_time\n # Speed: 0-255 maps to 0-2 cycles per second\n var cycles_per_second = tasmota.scale_uint(current_scale_speed, 0, 255, 0, 2)\n if cycles_per_second > 0\n self.scale_phase = (elapsed * cycles_per_second / 1000) % 256\n end\n end\n \n # Update source animation if it exists\n if current_source_animation != nil\n if !current_source_animation.is_running\n current_source_animation.start(self.start_time)\n end\n current_source_animation.update(time_ms)\n end\n \n # Calculate scaled colors\n self._calculate_scale()\n end\n \n # Calculate current scale factor based on mode\n def _get_current_scale_factor()\n var current_scale_mode = self.scale_mode\n var current_scale_factor = self.scale_factor\n \n if current_scale_mode == 0\n # Static scale\n return current_scale_factor\n elif current_scale_mode == 1\n # Oscillate between 0.5x and 2.0x\n var sine_val = self._sine(self.scale_phase)\n return tasmota.scale_uint(sine_val, 0, 255, 64, 255) # 0.5x to 2.0x\n elif current_scale_mode == 2\n # Grow from 0.5x to 2.0x\n return tasmota.scale_uint(self.scale_phase, 0, 255, 64, 255)\n else\n # Shrink from 2.0x to 0.5x\n return tasmota.scale_uint(255 - self.scale_phase, 0, 255, 64, 255)\n end\n end\n \n # Simple sine approximation\n def _sine(angle)\n # Simple sine approximation using quarter-wave symmetry\n var quarter = angle % 64\n if angle < 64\n return tasmota.scale_uint(quarter, 0, 64, 128, 255)\n elif angle < 128\n return tasmota.scale_uint(128 - angle, 0, 64, 128, 255)\n elif angle < 192\n return tasmota.scale_uint(angle - 128, 0, 64, 128, 0)\n else\n return tasmota.scale_uint(256 - angle, 0, 64, 128, 0)\n end\n end\n \n # Calculate scaled colors for all pixels\n def _calculate_scale()\n # Get current strip length from engine\n var current_strip_length = self.engine.strip_length\n \n # Ensure buffers are properly sized\n if size(self.current_colors) != current_strip_length\n self._initialize_buffers()\n end\n \n # Cache parameter values for performance\n var current_source_animation = self.source_animation\n var current_scale_center = self.scale_center\n var current_interpolation = self.interpolation\n \n # Clear source frame\n self.source_frame.clear()\n \n # Render source animation to frame\n if current_source_animation != nil\n current_source_animation.render(self.source_frame, 0)\n end\n \n # Get current scale factor\n var current_scale = self._get_current_scale_factor()\n \n # Calculate scale center in pixels\n var center_pixel = tasmota.scale_uint(current_scale_center, 0, 255, 0, current_strip_length - 1)\n \n # Apply scaling transformation\n var i = 0\n while i < current_strip_length\n # Calculate source position\n var distance_from_center = i - center_pixel\n # Scale: 128 = 1.0x, 64 = 0.5x, 255 = 2.0x\n var scaled_distance = tasmota.scale_uint(distance_from_center * 128, 0, 128 * 128, 0, current_scale * 128) / 128\n var source_pos = center_pixel + scaled_distance\n \n if current_interpolation == 0\n # Nearest neighbor\n if source_pos >= 0 && source_pos < current_strip_length\n self.current_colors[i] = self.source_frame.get_pixel_color(source_pos)\n else\n self.current_colors[i] = 0xFF000000\n end\n else\n # Linear interpolation using integer math\n if source_pos >= 0 && source_pos < current_strip_length - 1\n var pos_floor = int(source_pos)\n # Use integer fraction (0-255)\n var pos_frac_256 = int((source_pos - pos_floor) * 256)\n \n if pos_floor >= 0 && pos_floor < current_strip_length - 1\n var color1 = self.source_frame.get_pixel_color(pos_floor)\n var color2 = self.source_frame.get_pixel_color(pos_floor + 1)\n self.current_colors[i] = self._interpolate_colors(color1, color2, pos_frac_256)\n else\n self.current_colors[i] = 0xFF000000\n end\n else\n self.current_colors[i] = 0xFF000000\n end\n end\n \n i += 1\n end\n end\n \n # Interpolate between two colors using integer math\n def _interpolate_colors(color1, color2, factor_256)\n if factor_256 <= 0\n return color1\n elif factor_256 >= 256\n return color2\n end\n \n # Extract ARGB components\n var a1 = (color1 >> 24) & 0xFF\n var r1 = (color1 >> 16) & 0xFF\n var g1 = (color1 >> 8) & 0xFF\n var b1 = color1 & 0xFF\n \n var a2 = (color2 >> 24) & 0xFF\n var r2 = (color2 >> 16) & 0xFF\n var g2 = (color2 >> 8) & 0xFF\n var b2 = color2 & 0xFF\n \n # Interpolate each component using integer math\n var a = a1 + ((a2 - a1) * factor_256 / 256)\n var r = r1 + ((r2 - r1) * factor_256 / 256)\n var g = g1 + ((g2 - g1) * factor_256 / 256)\n var b = b1 + ((b2 - b1) * factor_256 / 256)\n \n return (a << 24) | (r << 16) | (g << 8) | b\n end\n \n # Render scale to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\n# Factory functions following parameterized class specification\n\n# Create a static scale animation preset\ndef scale_static(engine)\n var anim = animation.scale_animation(engine)\n anim.scale_mode = 0 # static mode\n anim.scale_speed = 0 # no animation\n return anim\nend\n\n# Create an oscillating scale animation preset\ndef scale_oscillate(engine)\n var anim = animation.scale_animation(engine)\n anim.scale_mode = 1 # oscillate mode\n anim.scale_speed = 128 # medium speed\n return anim\nend\n\n# Create a growing scale animation preset\ndef scale_grow(engine)\n var anim = animation.scale_animation(engine)\n anim.scale_mode = 2 # grow mode\n anim.scale_speed = 128 # medium speed\n return anim\nend\n\nreturn {'scale_animation': ScaleAnimation, 'scale_static': scale_static, 'scale_oscillate': scale_oscillate, 'scale_grow': scale_grow}"; modules["animations_future/shift.be"] = "# Shift animation effect for Berry Animation Framework\n#\n# This animation shifts/scrolls patterns horizontally across the LED strip\n# with configurable speed, direction, and wrapping behavior.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass ShiftAnimation : animation.animation\n # Non-parameter instance variables only\n var current_offset # Current shift offset in 1/256th pixels\n var source_frame # Frame buffer for source animation\n var current_colors # Array of current colors for each pixel\n \n # Parameter definitions with constraints\n static var PARAMS = animation.enc_params({\n \"source_animation\": {\"type\": \"instance\", \"default\": nil},\n \"shift_speed\": {\"min\": 0, \"max\": 255, \"default\": 128},\n \"direction\": {\"min\": -1, \"max\": 1, \"default\": 1},\n \"wrap_around\": {\"type\": \"bool\", \"default\": true}\n })\n \n # Initialize a new Shift animation\n def init(engine)\n # Call parent constructor with engine only\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.current_offset = 0\n self._initialize_buffers()\n end\n \n # Initialize buffers based on current strip length\n def _initialize_buffers()\n var current_strip_length = self.engine.strip_length\n self.source_frame = animation.frame_buffer(current_strip_length)\n self.current_colors = []\n self.current_colors.resize(current_strip_length)\n \n # Initialize colors to black\n var i = 0\n while i < current_strip_length\n self.current_colors[i] = 0xFF000000\n i += 1\n end\n end\n \n # Handle parameter changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n # Re-initialize buffers if strip length might have changed\n if name == \"source_animation\"\n self._initialize_buffers()\n end\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n \n # Cache parameter values for performance\n var current_shift_speed = self.shift_speed\n var current_direction = self.direction\n var current_wrap_around = self.wrap_around\n var current_source_animation = self.source_animation\n var current_strip_length = self.engine.strip_length\n \n # Update shift offset based on speed\n if current_shift_speed > 0\n var elapsed = time_ms - self.start_time\n # Speed: 0-255 maps to 0-10 pixels per second\n var pixels_per_second = tasmota.scale_uint(current_shift_speed, 0, 255, 0, 10 * 256)\n if pixels_per_second > 0\n var total_offset = (elapsed * pixels_per_second / 1000) * current_direction\n if current_wrap_around\n self.current_offset = total_offset % (current_strip_length * 256)\n if self.current_offset < 0\n self.current_offset += current_strip_length * 256\n end\n else\n self.current_offset = total_offset\n end\n end\n end\n \n # Update source animation if it exists\n if current_source_animation != nil\n if !current_source_animation.is_running\n current_source_animation.start(self.start_time)\n end\n current_source_animation.update(time_ms)\n end\n \n # Calculate shifted colors\n self._calculate_shift()\n end\n \n # Calculate shifted colors for all pixels\n def _calculate_shift()\n # Get current strip length and ensure buffers are correct size\n var current_strip_length = self.engine.strip_length\n if size(self.current_colors) != current_strip_length\n self._initialize_buffers()\n end\n \n # Cache parameter values\n var current_source_animation = self.source_animation\n var current_wrap_around = self.wrap_around\n \n # Clear source frame\n self.source_frame.clear()\n \n # Render source animation to frame\n if current_source_animation != nil\n current_source_animation.render(self.source_frame, 0)\n end\n \n # Apply shift transformation\n var pixel_offset = self.current_offset / 256 # Convert to pixel units\n var sub_pixel_offset = self.current_offset % 256 # Sub-pixel remainder\n \n var i = 0\n while i < current_strip_length\n var source_pos = i - pixel_offset\n \n if current_wrap_around\n # Wrap source position\n while source_pos < 0\n source_pos += current_strip_length\n end\n while source_pos >= current_strip_length\n source_pos -= current_strip_length\n end\n \n # Get color from wrapped position\n self.current_colors[i] = self.source_frame.get_pixel_color(source_pos)\n else\n # Clamp to strip bounds\n if source_pos >= 0 && source_pos < current_strip_length\n self.current_colors[i] = self.source_frame.get_pixel_color(source_pos)\n else\n self.current_colors[i] = 0xFF000000 # Black for out-of-bounds\n end\n end\n \n i += 1\n end\n end\n \n # Render shift to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\n# Factory functions\n\n# Create a shift animation that scrolls right\ndef shift_scroll_right(engine)\n var anim = animation.shift_animation(engine)\n anim.direction = 1\n anim.shift_speed = 128\n anim.wrap_around = true\n return anim\nend\n\n# Create a shift animation that scrolls left\ndef shift_scroll_left(engine)\n var anim = animation.shift_animation(engine)\n anim.direction = -1\n anim.shift_speed = 128\n anim.wrap_around = true\n return anim\nend\n\n# Create a fast scrolling shift animation\ndef shift_fast_scroll(engine)\n var anim = animation.shift_animation(engine)\n anim.direction = 1\n anim.shift_speed = 200\n anim.wrap_around = true\n return anim\nend\n\nreturn {\n 'shift_animation': ShiftAnimation,\n 'shift_scroll_right': shift_scroll_right,\n 'shift_scroll_left': shift_scroll_left,\n 'shift_fast_scroll': shift_fast_scroll\n}"; modules["animations_future/sparkle.be"] = "# Sparkle animation effect for Berry Animation Framework\n#\n# This animation creates random sparkles that appear and fade out over time,\n# with configurable density, fade speed, and colors.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass SparkleAnimation : animation.animation\n # Non-parameter instance variables only\n var current_colors # Array of current colors for each pixel\n var sparkle_states # Array of sparkle states for each pixel\n var sparkle_ages # Array of sparkle ages for each pixel\n var random_seed # Seed for random number generation\n var last_update # Last update time for frame timing\n \n # Parameter definitions following parameterized class specification\n static var PARAMS = animation.enc_params({\n \"color\": {\"default\": 0xFFFFFFFF},\n \"back_color\": {\"default\": 0xFF000000},\n \"density\": {\"min\": 0, \"max\": 255, \"default\": 30},\n \"fade_speed\": {\"min\": 0, \"max\": 255, \"default\": 50},\n \"sparkle_duration\": {\"min\": 0, \"max\": 255, \"default\": 60},\n \"min_brightness\": {\"min\": 0, \"max\": 255, \"default\": 100},\n \"max_brightness\": {\"min\": 0, \"max\": 255, \"default\": 255}\n })\n \n # Initialize a new Sparkle animation\n # @param engine: AnimationEngine - Required animation engine reference\n def init(engine)\n # Call parent constructor with engine only\n super(self).init(engine)\n \n # Initialize random seed using engine time\n self.random_seed = self.engine.time_ms % 65536\n \n # Initialize arrays and state - will be sized when strip length is known\n self.current_colors = []\n self.sparkle_states = [] # 0 = off, 1-255 = brightness\n self.sparkle_ages = [] # Age of each sparkle\n \n self.last_update = 0\n \n # Initialize buffers based on engine strip length\n self._initialize_buffers()\n end\n \n # Simple pseudo-random number generator\n def _random()\n self.random_seed = (self.random_seed * 1103515245 + 12345) & 0x7FFFFFFF\n return self.random_seed\n end\n \n # Get random number in range [0, max)\n def _random_range(max)\n if max <= 0\n return 0\n end\n return self._random() % max\n end\n \n # Initialize buffers based on current strip length\n def _initialize_buffers()\n var current_strip_length = self.engine.strip_length\n \n self.current_colors.resize(current_strip_length)\n self.sparkle_states.resize(current_strip_length)\n self.sparkle_ages.resize(current_strip_length)\n \n # Initialize all pixels\n var back_color = self.back_color\n var i = 0\n while i < current_strip_length\n self.current_colors[i] = back_color\n self.sparkle_states[i] = 0\n self.sparkle_ages[i] = 0\n i += 1\n end\n end\n \n # Override start method for timing control (acts as both start and restart)\n def start(time_ms)\n # Call parent start first (handles value_provider propagation)\n super(self).start(time_ms)\n \n # Reset random seed for consistent restarts\n self.random_seed = self.engine.time_ms % 65536\n \n # Reinitialize buffers in case strip length changed\n self._initialize_buffers()\n \n return self\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n \n # Update at approximately 30 FPS\n var update_interval = 33 # ~30 FPS\n if time_ms - self.last_update < update_interval\n return\n end\n self.last_update = time_ms\n \n # Update sparkle simulation\n self._update_sparkles(time_ms)\n end\n \n # Update sparkle states and create new sparkles\n def _update_sparkles(time_ms)\n var current_strip_length = self.engine.strip_length\n \n # Cache parameter values for performance\n var sparkle_duration = self.sparkle_duration\n var fade_speed = self.fade_speed\n var density = self.density\n var min_brightness = self.min_brightness\n var max_brightness = self.max_brightness\n var back_color = self.back_color\n \n var i = 0\n while i < current_strip_length\n # Update existing sparkles\n if self.sparkle_states[i] > 0\n self.sparkle_ages[i] += 1\n \n # Check if sparkle should fade or die\n if self.sparkle_ages[i] >= sparkle_duration\n # Sparkle has reached end of life\n self.sparkle_states[i] = 0\n self.sparkle_ages[i] = 0\n self.current_colors[i] = back_color\n else\n # Fade sparkle based on age and fade speed\n var age_ratio = tasmota.scale_uint(self.sparkle_ages[i], 0, sparkle_duration, 0, 255)\n var fade_factor = 255 - tasmota.scale_uint(age_ratio, 0, 255, 0, fade_speed)\n \n # Apply fade to brightness\n var new_brightness = tasmota.scale_uint(self.sparkle_states[i], 0, 255, 0, fade_factor)\n if new_brightness < 10\n # Sparkle too dim, turn off\n self.sparkle_states[i] = 0\n self.sparkle_ages[i] = 0\n self.current_colors[i] = back_color\n else\n # Update sparkle color with new brightness\n self._update_sparkle_color(i, new_brightness, time_ms)\n end\n end\n else\n # Check if new sparkle should appear\n if self._random_range(256) < density\n # Create new sparkle\n var brightness = min_brightness + self._random_range(max_brightness - min_brightness + 1)\n self.sparkle_states[i] = brightness\n self.sparkle_ages[i] = 0\n self._update_sparkle_color(i, brightness, time_ms)\n else\n # No sparkle, use background color\n self.current_colors[i] = back_color\n end\n end\n \n i += 1\n end\n end\n \n # Update color for a specific sparkle\n def _update_sparkle_color(pixel, brightness, time_ms)\n # Get base color using virtual parameter access\n var base_color = 0xFFFFFFFF\n \n # Access color parameter (automatically resolves value_providers)\n var color_param = self.color\n if animation.is_color_provider(color_param) && color_param.get_color_for_value != nil\n base_color = color_param.get_color_for_value(brightness, 0)\n else\n # Use the resolved color value with pixel influence for variation\n base_color = self.get_param_value(\"color\", time_ms + pixel * 10)\n end\n \n # Apply brightness scaling\n var a = (base_color >> 24) & 0xFF\n var r = (base_color >> 16) & 0xFF\n var g = (base_color >> 8) & 0xFF\n var b = base_color & 0xFF\n \n r = tasmota.scale_uint(brightness, 0, 255, 0, r)\n g = tasmota.scale_uint(brightness, 0, 255, 0, g)\n b = tasmota.scale_uint(brightness, 0, 255, 0, b)\n \n self.current_colors[pixel] = (a << 24) | (r << 16) | (g << 8) | b\n end\n \n # Render sparkles to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\n# Factory functions following parameterized class specification\n\n# Create a white sparkle animation preset\n# @param engine: AnimationEngine - Required animation engine reference\n# @return SparkleAnimation - A new white sparkle animation instance\ndef sparkle_white(engine)\n var anim = animation.sparkle_animation(engine)\n anim.color = 0xFFFFFFFF # white sparkles\n return anim\nend\n\n# Create a rainbow sparkle animation preset\n# @param engine: AnimationEngine - Required animation engine reference\n# @return SparkleAnimation - A new rainbow sparkle animation instance\ndef sparkle_rainbow(engine)\n var rainbow_provider = animation.rich_palette_color(engine)\n rainbow_provider.colors = animation.PALETTE_RAINBOW\n rainbow_provider.period = 5000\n rainbow_provider.transition_type = 1 # sine transition\n \n var anim = animation.sparkle_animation(engine)\n anim.color = rainbow_provider\n return anim\nend\n\nreturn {'sparkle_animation': SparkleAnimation, 'sparkle_white': sparkle_white, 'sparkle_rainbow': sparkle_rainbow}"; modules["animations_future/wave.be"] = "# Wave animation effect for Berry Animation Framework\n#\n# This animation creates various wave patterns (sine, triangle, square, sawtooth)\n# with configurable amplitude, frequency, phase, and movement speed.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass wave : animation.animation\n # Non-parameter instance variables only\n var current_colors # Array of current colors for each pixel\n var time_offset # Current time offset for movement\n var wave_table # Pre-computed wave table for performance\n \n # Parameter definitions for wave\n static var PARAMS = animation.enc_params({\n \"color\": {\"default\": 0xFFFF0000},\n \"back_color\": {\"default\": 0xFF000000},\n \"wave_type\": {\"min\": 0, \"max\": 3, \"default\": 0},\n \"amplitude\": {\"min\": 0, \"max\": 255, \"default\": 128},\n \"frequency\": {\"min\": 0, \"max\": 255, \"default\": 32},\n \"phase\": {\"min\": 0, \"max\": 255, \"default\": 0},\n \"wave_speed\": {\"min\": 0, \"max\": 255, \"default\": 50},\n \"center_level\": {\"min\": 0, \"max\": 255, \"default\": 128}\n })\n \n # Initialize a new Wave animation\n #\n # @param engine: AnimationEngine - The animation engine (required)\n def init(engine)\n log(\"ANI: `wave` animation is still in alpha and will be refactored\")\n # Call parent constructor\n super(self).init(engine)\n \n # Initialize non-parameter instance variables only\n self.current_colors = []\n self.time_offset = 0\n self.wave_table = []\n \n # Initialize wave table for performance\n self._init_wave_table()\n end\n \n # Initialize wave lookup tables for performance\n def _init_wave_table()\n self.wave_table.resize(256)\n \n var current_wave_type = self.wave_type\n \n var i = 0\n while i < 256\n # Generate different wave types\n var value = 0\n \n if current_wave_type == 0\n # Sine wave - using quarter-wave symmetry\n var quarter = i % 64\n if i < 64\n # First quarter: approximate sine\n value = tasmota.scale_uint(quarter, 0, 64, 128, 255)\n elif i < 128\n # Second quarter: mirror first quarter\n value = tasmota.scale_uint(128 - i, 0, 64, 128, 255)\n elif i < 192\n # Third quarter: negative first quarter\n value = tasmota.scale_uint(i - 128, 0, 64, 128, 0)\n else\n # Fourth quarter: negative second quarter\n value = tasmota.scale_uint(256 - i, 0, 64, 128, 0)\n end\n elif current_wave_type == 1\n # Triangle wave\n if i < 128\n value = tasmota.scale_uint(i, 0, 128, 0, 255)\n else\n value = tasmota.scale_uint(256 - i, 0, 128, 0, 255)\n end\n elif current_wave_type == 2\n # Square wave\n value = i < 128 ? 255 : 0\n else\n # Sawtooth wave\n value = i\n end\n \n self.wave_table[i] = value\n i += 1\n end\n end\n \n # Handle parameter changes\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"wave_type\"\n self._init_wave_table() # Regenerate wave table when wave type changes\n end\n end\n \n # Update animation state\n def update(time_ms)\n super(self).update(time_ms)\n \n # Update time offset based on wave speed\n var current_wave_speed = self.wave_speed\n if current_wave_speed > 0\n var elapsed = time_ms - self.start_time\n # Speed: 0-255 maps to 0-10 cycles per second\n var cycles_per_second = tasmota.scale_uint(current_wave_speed, 0, 255, 0, 10)\n if cycles_per_second > 0\n self.time_offset = (elapsed * cycles_per_second / 1000) % 256\n end\n end\n \n # Calculate wave colors\n self._calculate_wave(time_ms)\n end\n \n # Calculate wave colors for all pixels\n def _calculate_wave(time_ms)\n var strip_length = self.engine.strip_length\n var current_frequency = self.frequency\n var current_phase = self.phase\n var current_amplitude = self.amplitude\n var current_center_level = self.center_level\n var current_back_color = self.back_color\n var current_color = self.color\n \n # Resize current_colors array if needed\n if self.current_colors.size() != strip_length\n self.current_colors.resize(strip_length)\n end\n \n var i = 0\n while i < strip_length\n # Calculate wave position for this pixel\n var x = tasmota.scale_uint(i, 0, strip_length - 1, 0, 255)\n \n # Apply frequency scaling and phase offset\n var wave_pos = ((x * current_frequency / 32) + current_phase + self.time_offset) & 255\n \n # Get wave value from lookup table\n var wave_value = self.wave_table[wave_pos]\n \n # Apply amplitude scaling around center level\n var scaled_amplitude = tasmota.scale_uint(current_amplitude, 0, 255, 0, 128)\n var final_value = 0\n \n if wave_value >= 128\n # Upper half of wave\n var upper_amount = wave_value - 128\n upper_amount = tasmota.scale_uint(upper_amount, 0, 127, 0, scaled_amplitude)\n final_value = current_center_level + upper_amount\n else\n # Lower half of wave\n var lower_amount = 128 - wave_value\n lower_amount = tasmota.scale_uint(lower_amount, 0, 128, 0, scaled_amplitude)\n final_value = current_center_level - lower_amount\n end\n \n # Clamp to valid range\n if final_value > 255\n final_value = 255\n elif final_value < 0\n final_value = 0\n end\n \n # Get color from provider or use background\n var color = current_back_color\n if final_value > 10 # Threshold to avoid very dim colors\n # If the color is a provider that supports get_color_for_value, use it\n if animation.is_color_provider(current_color) && current_color.get_color_for_value != nil\n color = current_color.get_color_for_value(final_value, 0)\n else\n # Use resolve_value with wave influence\n color = self.resolve_value(current_color, \"color\", time_ms + final_value * 10)\n \n # Apply wave intensity as brightness scaling\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n r = tasmota.scale_uint(final_value, 0, 255, 0, r)\n g = tasmota.scale_uint(final_value, 0, 255, 0, g)\n b = tasmota.scale_uint(final_value, 0, 255, 0, b)\n \n color = (a << 24) | (r << 16) | (g << 8) | b\n end\n end\n \n self.current_colors[i] = color\n i += 1\n end\n end\n \n # Render wave to frame buffer\n def render(frame, time_ms, strip_length)\n var i = 0\n while i < strip_length\n if i < frame.width && i < self.current_colors.size()\n frame.set_pixel_color(i, self.current_colors[i])\n end\n i += 1\n end\n \n return true\n end\nend\n\nreturn {'wave': wave}"; modules["autoexec.be"] = "import tasmota\ndef log(x) print(x) end\nimport animation\nimport animation_dsl\n"; modules["core/animation_base.be"] = "# Animation base class - The unified root of the animation hierarchy\n# \n# An Animation defines WHAT should be displayed and HOW it changes over time.\n# Animations can generate colors for any pixel at any time, have priority for layering,\n# and can be rendered directly. They also support temporal behavior like duration and looping.\n# \n# This is the unified base class for all visual elements in the framework.\n# A Pattern is simply an Animation with infinite duration (duration = 0).\n#\n# Extends parameterized_object to provide parameter management and playable interface.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass Animation : animation.parameterized_object\n # Non-parameter instance variables only\n var opacity_frame # Frame buffer for opacity animation rendering\n \n # Parameter definitions (extends Playable's PARAMS)\n static var PARAMS = animation.enc_params({\n # Inherited from Playable: is_running\n \"id\": {\"type\": \"string\", \"default\": \"\"}, # Optional id for the animation\n \"priority\": {\"min\": 0, \"default\": 10}, # Rendering priority (higher = on top, 0-255)\n \"duration\": {\"min\": 0, \"default\": 0}, # Animation duration in ms (0 = infinite)\n \"loop\": {\"type\": \"bool\", \"default\": false}, # Whether to loop when duration is reached\n \"opacity\": {\"type\": \"any\", \"default\": 255}, # Animation opacity (0-255 number or Animation instance)\n \"color\": {\"default\": 0x00000000} # Base color in ARGB format (0xAARRGGBB) - default to transparent\n })\n\n # Initialize a new animation\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n # Initialize parameter system with engine\n super(self).init(engine)\n \n # Initialize non-parameter instance variables (none currently)\n end\n \n # Update animation state based on current time\n # This method should be called regularly by the animation engine\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Access parameters via virtual members\n var current_duration = self.duration\n \n # Check if animation has completed its duration\n if current_duration > 0\n var elapsed = time_ms - self.start_time\n if elapsed >= current_duration\n var current_loop = self.loop\n if current_loop\n # Reset start time to create a looping effect\n # We calculate the precise new start time to avoid drift\n var loops_completed = elapsed / current_duration\n self.start_time = self.start_time + (loops_completed * current_duration)\n else\n # Animation completed, make it inactive\n # Set directly in values map to avoid triggering on_param_changed\n self.is_running = false\n end\n end\n end\n end\n \n # Render the animation to the provided frame buffer\n # Default implementation renders a solid color (makes Animation equivalent to solid pattern)\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n # Access parameters via virtual members (auto-resolves value_providers)\n var current_color = self.member(\"color\")\n \n # Fill the entire frame with the current color if not transparent\n if (current_color != 0x00000000)\n frame.fill_pixels(frame.pixels, current_color)\n end\n \n return true\n end\n \n # Post-processing of rendering\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n def post_render(frame, time_ms, strip_length)\n # no need to auto-fix time_ms and start_time\n # Handle opacity - can be number, frame buffer, or animation\n var current_opacity = self.opacity\n if (current_opacity == 255)\n return # nothing to do\n elif type(current_opacity) == 'int'\n # Number mode: apply uniform opacity\n frame.apply_opacity(frame.pixels, current_opacity)\n else\n # Opacity is a frame buffer\n self._apply_opacity(frame, current_opacity, time_ms, strip_length)\n end\n end\n\n # Apply opacity to frame buffer - handles numbers and animations\n #\n # @param frame: frame_buffer - The frame buffer to apply opacity to\n # @param opacity: int|Animation - Opacity value or animation\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels\n def _apply_opacity(frame, opacity, time_ms, strip_length)\n # Check if opacity is an animation instance\n if isinstance(opacity, animation.animation)\n # Animation mode: render opacity animation to frame buffer and use as mask\n var opacity_animation = opacity\n \n # Ensure opacity frame buffer exists and has correct size\n if self.opacity_frame == nil || self.opacity_frame.width != frame.width\n self.opacity_frame = animation.frame_buffer(frame.width)\n end\n \n # Clear and render opacity animation to frame buffer\n self.opacity_frame.clear()\n \n # Start opacity animation if not running\n if !opacity_animation.is_running\n opacity_animation.start(self.start_time)\n end\n \n # Update and render opacity animation\n opacity_animation.update(time_ms)\n opacity_animation.render(self.opacity_frame, time_ms, strip_length)\n \n # Use rendered frame buffer as opacity mask\n frame.apply_opacity(frame.pixels, self.opacity_frame.pixels)\n end\n end\n \n # Get a color for a specific pixel position and time\n # Default implementation returns the animation's color (solid color for all pixels)\n #\n # @param pixel: int - Pixel index (0-based)\n # @param time_ms: int - Current time in milliseconds\n # @return int - Color in ARGB format (0xAARRGGBB)\n def get_color_at(pixel, time_ms)\n return self.get_param_value(\"color\", time_ms)\n end\n \n # Get a color based on time (convenience method)\n #\n # @param time_ms: int - Current time in milliseconds\n # @return int - Color in ARGB format (0xAARRGGBB)\n def get_color(time_ms)\n return self.get_color_at(0, time_ms)\n end\n \nend\n\nreturn {'animation': Animation}\n"; modules["core/animation_engine.be"] = "# Unified Animation Engine\n#\n# Uses composition pattern: contains a root engine_proxy that manages all children.\n# The engine provides infrastructure (strip output, fast_loop) while delegating\n# child management and rendering to the root animation.\n\nclass AnimationEngine\n # Minimum milliseconds between ticks\n static var TICK_MS = 50\n \n # Core properties\n var strip # LED strip object\n var strip_length # Strip length (cached for performance)\n var root_animation # Root engine_proxy that holds all children\n var frame_buffer # Main frame buffer\n var temp_buffer # Temporary buffer for blending\n \n # State management\n var is_running # Whether engine is active\n var last_update # Last update time in milliseconds\n var time_ms # Current time in milliseconds (updated each frame)\n var fast_loop_closure # Stored closure for fast_loop registration\n var tick_ms # Minimum milliseconds between ticks (runtime configurable)\n \n # Performance optimization\n var render_needed # Whether a render pass is needed\n \n # CPU metrics tracking (streaming stats - no array storage)\n var tick_count # Number of ticks in current period\n var tick_time_sum # Sum of all tick times (for mean calculation)\n var tick_time_min # Minimum tick time in period\n var tick_time_max # Maximum tick time in period\n var anim_time_sum # Sum of animation calculation times\n var anim_time_min # Minimum animation calculation time\n var anim_time_max # Maximum animation calculation time\n var hw_time_sum # Sum of hardware output times\n var hw_time_min # Minimum hardware output time\n var hw_time_max # Maximum hardware output time\n \n # Intermediate measurement point metrics\n var phase1_time_sum # Sum of phase 1 times (ts_start to ts_1)\n var phase1_time_min # Minimum phase 1 time\n var phase1_time_max # Maximum phase 1 time\n var phase2_time_sum # Sum of phase 2 times (ts_1 to ts_2)\n var phase2_time_min # Minimum phase 2 time\n var phase2_time_max # Maximum phase 2 time\n var phase3_time_sum # Sum of phase 3 times (ts_2 to ts_3)\n var phase3_time_min # Minimum phase 3 time\n var phase3_time_max # Maximum phase 3 time\n \n var last_stats_time # Last time stats were printed\n var stats_period # Stats reporting period (5000ms)\n \n # Profiling timestamps (only store timestamps, compute durations in _record_tick_metrics)\n var ts_start # Timestamp: tick start\n var ts_1 # Timestamp: intermediate measure point 1 (optional)\n var ts_2 # Timestamp: intermediate measure point 2 (optional)\n var ts_3 # Timestamp: intermediate measure point 3 (optional)\n var ts_hw # Timestamp: hardware output complete\n var ts_end # Timestamp: tick end\n \n # Initialize the animation engine for a specific LED strip\n def init(strip)\n if strip == nil\n raise \"value_error\", \"strip cannot be nil\"\n end\n \n self.strip = strip\n self.strip_length = strip.length()\n \n # Create frame buffers\n self.frame_buffer = animation.frame_buffer(self.strip_length)\n self.temp_buffer = animation.frame_buffer(self.strip_length)\n \n # Create root engine_proxy to manage all children\n self.root_animation = animation.engine_proxy(self)\n \n # Initialize state\n self.is_running = false\n self.last_update = 0\n self.time_ms = 0\n self.fast_loop_closure = nil\n self.tick_ms = self.TICK_MS # Initialize from static default\n self.render_needed = false\n \n # Initialize CPU metrics\n self.tick_count = 0\n self.tick_time_sum = 0\n self.tick_time_min = 999999\n self.tick_time_max = 0\n self.anim_time_sum = 0\n self.anim_time_min = 999999\n self.anim_time_max = 0\n self.hw_time_sum = 0\n self.hw_time_min = 999999\n self.hw_time_max = 0\n \n # Initialize intermediate phase metrics\n self.phase1_time_sum = 0\n self.phase1_time_min = 999999\n self.phase1_time_max = 0\n self.phase2_time_sum = 0\n self.phase2_time_min = 999999\n self.phase2_time_max = 0\n self.phase3_time_sum = 0\n self.phase3_time_min = 999999\n self.phase3_time_max = 0\n \n self.last_stats_time = 0\n self.stats_period = 5000\n \n # Initialize profiling timestamps\n self.ts_start = nil\n self.ts_1 = nil\n self.ts_2 = nil\n self.ts_3 = nil\n self.ts_hw = nil\n self.ts_end = nil\n end\n \n # Run the animation engine\n # \n # @return self for method chaining\n def run()\n if !self.is_running\n var now = tasmota.millis()\n self.is_running = true\n self.last_update = now - 10\n \n if self.fast_loop_closure == nil\n self.fast_loop_closure = / -> self.on_tick()\n end\n\n # Start the root animation (which starts all children)\n self.root_animation.start(now)\n \n tasmota.add_fast_loop(self.fast_loop_closure)\n end\n return self\n end\n \n # Stop the animation engine\n # \n # @return self for method chaining\n def stop()\n if self.is_running\n self.is_running = false\n \n if self.fast_loop_closure != nil\n tasmota.remove_fast_loop(self.fast_loop_closure)\n end\n end\n return self\n end\n \n # Add an animation or sequence to the root animation\n # \n # @param obj: Animation|sequence_manager - The object to add\n # @return bool - True if added, false if already exists\n def add(obj)\n var ret = self.root_animation.add(obj)\n if ret\n self.render_needed = true\n end\n return ret\n end\n \n # Remove an animation or sequence from the root animation\n # \n # @param obj: Animation|sequence_manager - The object to remove\n # @return bool - True if removed, false if not found\n def remove(obj)\n var ret = self.root_animation.remove(obj)\n if ret\n self.render_needed = true\n end\n return ret\n end\n \n # Clear all animations and sequences\n def clear()\n # Stop and clear all children in root animation\n self.root_animation.clear()\n self.render_needed = true\n return self\n end\n \n # Main tick function called by fast_loop\n def on_tick(current_time)\n if !self.is_running\n return false\n end\n \n if current_time == nil\n current_time = tasmota.millis()\n end\n \n # Throttle updates based on tick_ms setting\n var delta_time = current_time - self.last_update\n if delta_time < self.tick_ms\n return true\n end\n \n # Start timing this tick (use tasmota.millis() for consistent profiling)\n self.ts_start = tasmota.millis()\n \n # Check if strip length changed since last time\n self.check_strip_length()\n \n # Update engine time\n self.time_ms = current_time\n \n self.last_update = current_time\n \n # Check if strip can accept updates\n if self.strip.can_show != nil && !self.strip.can_show()\n return true\n end\n \n # Process any queued events (non-blocking)\n self._process_events(current_time)\n \n # Update and render root animation (which updates all children)\n self._update_and_render(current_time)\n \n # End timing and record metrics\n self.ts_end = tasmota.millis()\n self._record_tick_metrics(current_time)\n \n global.debug_animation = false\n return true\n end\n \n # Unified update and render process\n def _update_and_render(time_ms)\n self.ts_1 = tasmota.millis()\n # Update root animation (which updates all children)\n self.root_animation.update(time_ms)\n \n self.ts_2 = tasmota.millis()\n # Skip rendering if no children\n if self.root_animation.is_empty()\n if self.render_needed\n self._clear_strip()\n self.render_needed = false\n end\n return\n end\n \n # Clear main buffer\n self.frame_buffer.clear()\n \n # self.ts_2 = tasmota.millis()\n # Render root animation (which renders all children with blending)\n var rendered = self.root_animation.render(self.frame_buffer, time_ms)\n \n self.ts_3 = tasmota.millis()\n # Output to hardware and measure time\n self._output_to_strip()\n self.ts_hw = tasmota.millis()\n \n self.render_needed = false\n end\n \n # Output frame buffer to LED strip\n def _output_to_strip()\n self.strip.push_pixels_buffer_argb(self.frame_buffer.pixels)\n self.strip.show()\n end\n \n # Clear the LED strip\n def _clear_strip()\n self.strip.clear()\n self.strip.show()\n end\n \n # Event processing methods\n def _process_events(current_time)\n # Process any queued events from the animation event manager\n # This is called during fast_loop to handle events asynchronously\n if animation.event_manager != nil\n animation.event_manager._process_queued_events()\n end\n end\n \n # Record tick metrics and print stats periodically\n def _record_tick_metrics(current_time)\n # Compute durations from timestamps (only if timestamps are not nil)\n var tick_duration = nil\n var anim_duration = nil\n var hw_duration = nil\n var phase1_duration = nil\n var phase2_duration = nil\n var phase3_duration = nil\n \n # Total tick duration: from start to end\n if self.ts_start != nil && self.ts_end != nil\n tick_duration = self.ts_end - self.ts_start\n end\n \n # Animation duration: from ts_2 (after event processing) to ts_3 (before hardware)\n if self.ts_2 != nil && self.ts_3 != nil\n anim_duration = self.ts_3 - self.ts_2\n end\n \n # Hardware duration: from ts_3 (before hardware) to ts_hw (after hardware)\n if self.ts_3 != nil && self.ts_hw != nil\n hw_duration = self.ts_hw - self.ts_3\n end\n \n # Phase 1: from ts_start to ts_1 (initial checks)\n if self.ts_start != nil && self.ts_1 != nil\n phase1_duration = self.ts_1 - self.ts_start\n end\n \n # Phase 2: from ts_1 to ts_2 (event processing)\n if self.ts_1 != nil && self.ts_2 != nil\n phase2_duration = self.ts_2 - self.ts_1\n end\n \n # Phase 3: from ts_2 to ts_3 (animation update/render)\n if self.ts_2 != nil && self.ts_3 != nil\n phase3_duration = self.ts_3 - self.ts_2\n end\n \n # Initialize stats time on first tick\n if self.last_stats_time == 0\n self.last_stats_time = current_time\n end\n \n # Update streaming statistics (only if durations are valid)\n self.tick_count += 1\n \n if tick_duration != nil\n self.tick_time_sum += tick_duration\n if tick_duration < self.tick_time_min\n self.tick_time_min = tick_duration\n end\n if tick_duration > self.tick_time_max\n self.tick_time_max = tick_duration\n end\n end\n \n if anim_duration != nil\n self.anim_time_sum += anim_duration\n if anim_duration < self.anim_time_min\n self.anim_time_min = anim_duration\n end\n if anim_duration > self.anim_time_max\n self.anim_time_max = anim_duration\n end\n end\n \n if hw_duration != nil\n self.hw_time_sum += hw_duration\n if hw_duration < self.hw_time_min\n self.hw_time_min = hw_duration\n end\n if hw_duration > self.hw_time_max\n self.hw_time_max = hw_duration\n end\n end\n \n # Update phase metrics\n if phase1_duration != nil\n self.phase1_time_sum += phase1_duration\n if phase1_duration < self.phase1_time_min\n self.phase1_time_min = phase1_duration\n end\n if phase1_duration > self.phase1_time_max\n self.phase1_time_max = phase1_duration\n end\n end\n \n if phase2_duration != nil\n self.phase2_time_sum += phase2_duration\n if phase2_duration < self.phase2_time_min\n self.phase2_time_min = phase2_duration\n end\n if phase2_duration > self.phase2_time_max\n self.phase2_time_max = phase2_duration\n end\n end\n \n if phase3_duration != nil\n self.phase3_time_sum += phase3_duration\n if phase3_duration < self.phase3_time_min\n self.phase3_time_min = phase3_duration\n end\n if phase3_duration > self.phase3_time_max\n self.phase3_time_max = phase3_duration\n end\n end\n \n # Check if it's time to print stats (every 5 seconds)\n var time_since_stats = current_time - self.last_stats_time\n if time_since_stats >= self.stats_period\n self._print_stats(time_since_stats)\n \n # Reset for next period\n self.tick_count = 0\n self.tick_time_sum = 0\n self.tick_time_min = 999999\n self.tick_time_max = 0\n self.anim_time_sum = 0\n self.anim_time_min = 999999\n self.anim_time_max = 0\n self.hw_time_sum = 0\n self.hw_time_min = 999999\n self.hw_time_max = 0\n self.phase1_time_sum = 0\n self.phase1_time_min = 999999\n self.phase1_time_max = 0\n self.phase2_time_sum = 0\n self.phase2_time_min = 999999\n self.phase2_time_max = 0\n self.phase3_time_sum = 0\n self.phase3_time_min = 999999\n self.phase3_time_max = 0\n self.last_stats_time = current_time\n end\n end\n \n # Print CPU statistics\n def _print_stats(period_ms)\n if self.tick_count == 0\n return\n end\n \n # # Calculate statistics\n # var expected_ticks = period_ms / 5 # Expected ticks at 5ms intervals\n # var missed_ticks = expected_ticks - self.tick_count\n \n # Calculate means from sums\n var mean_time = self.tick_time_sum / self.tick_count\n var mean_anim = self.anim_time_sum / self.tick_count\n var mean_hw = self.hw_time_sum / self.tick_count\n\n var mean_phase1 = self.phase1_time_sum / self.tick_count\n var mean_phase2 = self.phase2_time_sum / self.tick_count\n var mean_phase3 = self.phase3_time_sum / self.tick_count\n \n # # Calculate CPU usage percentage\n # var cpu_percent = (self.tick_time_sum * 100) / period_ms\n \n # Format and log stats - split into animation calc vs hardware output\n 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})\"\n tasmota.log(stats_msg, 3) # Log level 3 (DEBUG)\n end\n \n # Interrupt current animations\n def interrupt_current()\n self.root_animation.stop()\n end\n \n # Interrupt specific animation by name\n def interrupt_animation(id)\n var i = 0\n while i < size(self.root_animation.children)\n var child = self.root_animation.children[i]\n if isinstance(child, animation.animation) && child.id == id\n child.stop()\n self.root_animation.children.remove(i)\n return\n end\n i += 1\n end\n end\n \n # Resume animations (placeholder for future state management)\n def resume()\n # For now, just ensure engine is running\n if !self.is_running\n self.start()\n end\n end\n \n # Resume after a delay (placeholder for future implementation)\n def resume_after(delay_ms)\n tasmota.set_timer(delay_ms, def () self.resume() end)\n end\n \n # Utility methods for compatibility\n def get_strip()\n return self.strip\n end\n \n def get_strip_length()\n return self.strip_length\n end\n \n def is_active()\n return self.is_running\n end\n \n def size()\n # Count only animations, not sequences (for backward compatibility)\n return self.root_animation.size_animations()\n end\n \n def get_animations()\n return self.root_animation.get_animations()\n end\n \n # Backward compatibility: get sequence managers\n def sequence_managers()\n return self.root_animation.sequences\n end\n \n # Backward compatibility: get animations list\n def animations()\n return self.get_animations()\n end\n \n # Check if the length of the strip changes\n #\n # @return bool - True if strip lengtj was changed, false otherwise\n def check_strip_length()\n var current_length = self.strip.length()\n if current_length != self.strip_length\n self._handle_strip_length_change(current_length)\n return true # Length changed\n end\n return false # No change\n end\n \n # Handle strip length changes by resizing buffers\n def _handle_strip_length_change(new_length)\n if new_length <= 0\n return # Invalid length, ignore\n end\n \n self.strip_length = new_length\n \n # Resize existing frame buffers instead of creating new ones\n self.frame_buffer.resize(new_length)\n self.temp_buffer.resize(new_length)\n \n # Force a render to clear any stale pixels\n self.render_needed = true\n end\n \n # Cleanup method for proper resource management\n def cleanup()\n self.stop()\n self.clear()\n self.frame_buffer = nil\n self.temp_buffer = nil\n self.strip = nil\n end\n \n # Sequence iteration tracking methods, delegate to engine_proxy\n \n # Push a new iteration context onto the stack\n # Called when a sequence starts repeating\n #\n # @param iteration_number: int - The current iteration number (0-based)\n def push_iteration_context(iteration_number)\n return self.root_animation.push_iteration_context(iteration_number)\n end\n \n # Pop the current iteration context from the stack\n # Called when a sequence finishes repeating\n def pop_iteration_context()\n return self.root_animation.pop_iteration_context()\n end\n \n # Update the current iteration number in the top context\n # Called when a sequence advances to the next iteration\n #\n # @param iteration_number: int - The new iteration number (0-based)\n def update_current_iteration(iteration_number)\n return self.root_animation.update_current_iteration(iteration_number)\n end\n \n # Get the current iteration number from the innermost sequence context\n # Used by iteration_number to return the current iteration\n #\n # @return int|nil - Current iteration number (0-based) or nil if not in sequence\n def get_current_iteration_number()\n return self.root_animation.get_current_iteration_number()\n end\n \n # String representation\n def tostring()\n return f\"AnimationEngine(running={self.is_running})\"\n end\nend\n\nreturn {'create_engine': AnimationEngine}"; modules["core/engine_proxy.be"] = "# Engine Proxy - Combines rendering and orchestration\n# \n# An engine_proxy is a Playable that can both render visual content\n# AND orchestrate sub-animations and sequences. This enables complex\n# composite effects that combine multiple animations with timing control.\n#\n# Example use cases:\n# - An animation that renders a background while orchestrating foreground effects\n# - A composite effect that switches between different animations over time\n# - A complex pattern that combines multiple sub-animations with sequences\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass engine_proxy : animation.animation\n # Non-parameter instance variables\n var animations # List of child animations\n var sequences # List of child sequence managers\n var value_providers # List of value providers that need update() calls\n var strip_length # Proxy for strip_length from engine\n var temp_buffer # proxy for the global 'engine.temp_buffer' used as a scratchad buffer during rendering, this object is maintained over time to avoid new objects creation\n \n # Sequence iteration tracking (stack-based for nested sequences)\n var iteration_stack # Stack of iteration numbers for nested sequences\n \n # Cached time for child access (updated during update())\n var time_ms # Current time in milliseconds (cached from engine)\n \n def init(engine)\n # Initialize parameter system with engine\n super(self).init(engine)\n \n # Keep a reference of 'engine.temp_buffer'\n self.temp_buffer = self.engine.temp_buffer\n\n # Initialize non-parameter instance variables\n self.animations = []\n self.sequences = []\n self.value_providers = []\n \n # Initialize iteration tracking stack\n self.iteration_stack = []\n \n # Initialize time cache\n self.time_ms = 0\n \n # Call template setup method (empty placeholder for subclasses)\n self.setup_template()\n end\n \n # Template setup method - empty placeholder for template animations\n # Template animations override this method to set up their animations and sequences\n def setup_template()\n # Empty placeholder - template animations override this method\n end\n \n # Is empty\n #\n # @return true if animations, sequences, and value_providers are all empty\n def is_empty()\n return (size(self.animations) == 0) && (size(self.sequences) == 0) && (size(self.value_providers) == 0)\n end\n\n # Number of animations\n #\n # @return true both animations and sequences are empty\n def size_animations()\n return size(self.animations)\n end\n\n def get_animations()\n # Return only Animation children (not SequenceManagers)\n var anims = []\n for child : self.animations\n if isinstance(child, animation.animation)\n anims.push(child)\n end\n end\n return anims\n end\n \n # Add a child animation, sequence, or value provider\n #\n # @param obj: Animation|sequence_manager|value_provider - The child to add\n # @return self for method chaining\n def add(obj)\n if isinstance(obj, animation.sequence_manager)\n return self._add_sequence_manager(obj)\n # Check if it's a value_provider (before Animation check, as some animations might also be providers)\n elif animation.is_value_provider(obj)\n return self._add_value_provider(obj)\n # Check if it's an Animation (or subclass)\n elif isinstance(obj, animation.animation)\n return self._add_animation(obj)\n else\n # Unknown type - provide helpful error message\n raise \"type_error\", \"only Animation, sequence_manager, or value_provider\"\n end\n end\n\n # Add a sequence manager\n def _add_sequence_manager(sequence_manager)\n if (self.sequences.find(sequence_manager) == nil)\n self.sequences.push(sequence_manager)\n return true\n else\n return false\n end\n end\n\n # Add a value provider\n #\n # @param provider: value_provider - The value provider instance to add\n # @return true if successful, false if already in list\n def _add_value_provider(provider)\n if (self.value_providers.find(provider) == nil)\n self.value_providers.push(provider)\n # Note: We don't start the provider here - it's started by the animation that uses it\n # We only register it so its update() method gets called in the update loop\n return true\n else\n return false\n end\n end\n\n # Add an animation with automatic priority sorting\n # \n # @param anim: animation - The animation instance to add (if not already listed)\n # @return true if succesful (TODO always true)\n def _add_animation(anim)\n if (self.animations.find(anim) == nil) # not already in list\n # Add and sort by priority (higher priority first)\n self.animations.push(anim)\n self._sort_animations_by_priority()\n # If the engine is already started, auto-start the animation\n if self.is_running\n anim.start(self.engine.time_ms)\n end\n return true\n else\n return false\n end\n end\n \n # Sort animations by priority (animations only, sequences don't have priority)\n # Higher priority animations render on top\n def _sort_animations_by_priority()\n var n = size(self.animations)\n if n <= 1\n return\n end\n \n # Insertion sort for small lists\n # Only sort animations (not sequences), keep sequences at end\n var i = 1\n while i < n\n var key = self.animations[i]\n \n # Skip if key is not an animation\n if !isinstance(key, animation.animation)\n i += 1\n continue\n end\n \n var j = i\n while j > 0\n var prev = self.animations[j-1]\n # Stop if previous is not an animation or has higher/equal priority\n if !isinstance(prev, animation.animation) || prev.priority >= key.priority # todo is test still useful?\n break\n end\n self.animations[j] = self.animations[j-1]\n j -= 1\n end\n self.animations[j] = key\n i += 1\n end\n end\n \n # Remove a child animation\n #\n # @param obj: Animation - The animation to remove\n # @return true if actually removed\n def _remove_animation(obj)\n var idx = self.animations.find(obj)\n if idx != nil\n self.animations.remove(idx)\n return true\n else\n return false\n end\n end\n \n # Remove a sequence manager\n #\n # @param obj: Sequence Manager instance\n # @return true if actually removed\n def _remove_sequence_manager(obj)\n var idx = self.sequences.find(obj)\n if idx != nil\n self.sequences.remove(idx)\n return true\n else\n return false\n end\n end\n\n # Remove a value provider\n #\n # @param obj: value_provider instance\n # @return true if actually removed\n def _remove_value_provider(obj)\n var idx = self.value_providers.find(obj)\n if idx != nil\n self.value_providers.remove(idx)\n return true\n else\n return false\n end\n end\n\n # Generic remove method that delegates to specific remove methods\n # @param obj: Animation, sequence_manager, or value_provider - The object to remove\n # @return self for method chaining\n def remove(obj)\n # Check if it's a sequence_manager\n if isinstance(obj, animation.sequence_manager)\n return self._remove_sequence_manager(obj)\n # Check if it's a value_provider (before Animation check)\n elif animation.is_value_provider(obj)\n return self._remove_value_provider(obj)\n # Check if it's an Animation (or subclass)\n elif isinstance(obj, animation.animation)\n return self._remove_animation(obj)\n else\n # Unknown type - ignore\n end\n end\n\n # Start the hybrid animation and all its children\n #\n # @param time_ms: int - Start time in milliseconds\n # @return self for method chaining\n def start(time_ms)\n # Call parent start\n super(self).start(time_ms)\n \n # Note: We don't start value_providers here - they are started by the animations that use them\n # Value providers are only registered here so their update() method gets called\n \n # Start all sequences FIRST (they may control animations)\n var idx = 0\n while idx < size(self.sequences)\n self.sequences[idx].start(time_ms)\n idx += 1\n end\n\n # Start all value providers SECOND (they provide dynamic values)\n idx = 0\n while idx < size(self.value_providers)\n self.value_providers[idx].start(time_ms)\n idx += 1\n end\n\n # Start all animations THIRD (they use values from providers and sequences)\n idx = 0\n while idx < size(self.animations)\n self.animations[idx].start(time_ms)\n idx += 1\n end\n \n return self\n end\n \n # Stop the hybrid animation and all its children\n #\n # @return self for method chaining\n def stop()\n # Stop all animations FIRST (they depend on sequences and value providers)\n var idx = 0\n while idx < size(self.animations)\n self.animations[idx].stop()\n idx += 1\n end\n\n # Stop all sequences SECOND (they may control animations)\n idx = 0\n while idx < size(self.sequences)\n self.sequences[idx].stop()\n idx += 1\n end\n\n # Note: We don't stop value_providers here - they are stopped by the animations that use them\n # Value providers are only registered here so their update() method gets called\n \n # Call parent stop\n super(self).stop()\n \n return self\n end\n \n # Stop and clear the hybrid animation and all its children\n #\n # @return self for method chaining\n def clear()\n self.stop()\n self.animations = []\n self.sequences = []\n self.value_providers = []\n\n return self\n end\n\n # Update the hybrid animation and all its children\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Cache time for child access\n self.time_ms = time_ms # We have 'self.time' attribute to mimick 'engine' behavior\n self.strip_length = self.engine.strip_length # We have 'self.strip_length' attribute to mimick 'engine' behavior\n \n # Update parent animation state\n super(self).update(time_ms)\n \n # Update all value providers FIRST (they may produce values used by sequences and animations)\n var idx = 0\n var sz = size(self.value_providers)\n while idx < sz\n var vp = self.value_providers[idx]\n if vp.is_running\n # Set start time if needed\n if vp.start_time == nil\n vp.start_time = time_ms\n end\n # Call actual update\n vp.update(time_ms)\n end\n idx += 1\n end\n \n # Update all child sequences SECOND (they may control animations)\n idx = 0\n sz = size(self.sequences)\n while idx < sz\n var sq = self.sequences[idx]\n if sq.is_running\n # Set start time if needed\n if sq.start_time == nil\n sq.start_time = time_ms\n end\n # Call actual update\n sq.update(time_ms)\n end\n idx += 1\n end\n \n # Update all child animations LAST (they use values from providers and sequences)\n idx = 0\n sz = size(self.animations)\n while idx < sz\n var an = self.animations[idx]\n if an.is_running\n # Set start time if needed\n if an.start_time == nil\n an.start_time = time_ms\n end\n # Call actual update\n an.update(time_ms)\n end\n idx += 1\n end\n end\n \n # Render the hybrid animation\n # Renders own content first, then all child animations\n #\n # @param frame: frame_buffer - The frame buffer to render to\n # @param time_ms: int - Current time in milliseconds\n # @param strip_length: int - Length of the LED strip in pixels (optional, defaults to self.strip_length)\n # @return bool - True if frame was modified, false otherwise\n def render(frame, time_ms, strip_length)\n if !self.is_running || frame == nil\n return false\n end\n\n # Use cached strip_length if not provided\n if strip_length == nil\n strip_length = self.strip_length\n end\n\n # # update sequences first\n # var i = 0\n # while i < size(self.sequences)\n # self.sequences[i].update(time_ms)\n # i += 1\n # end\n \n var modified = false\n \n # We don't call super method for optimization, skipping color computation\n # modified = super(self).render(frame, time_ms, strip_length)\n \n # Render all child animations (but not sequences - they don't render)\n var idx = 0\n var sz = size(self.animations)\n while idx < sz\n var child = self.animations[idx]\n\n if child.is_running\n # Clear temporary buffer with transparent\n self.temp_buffer.clear()\n\n # Render child\n var child_rendered = child.render(self.temp_buffer, time_ms, strip_length)\n \n if child_rendered\n # Apply child's post-processing\n child.post_render(self.temp_buffer, time_ms, strip_length)\n \n # Blend child into main frame\n frame.blend_pixels(frame.pixels, self.temp_buffer.pixels)\n modified = true\n end\n end\n idx += 1\n end\n \n return modified\n end\n \n # Delegation methods to engine (for compatibility with child objects)\n \n # Get strip length from engine\n def get_strip_length()\n return self.engine.strip_length\n end\n \n # Sequence iteration tracking methods\n \n # Push a new iteration context onto the stack\n # Called when a sequence starts repeating\n #\n # @param iteration_number: int - The current iteration number (0-based)\n def push_iteration_context(iteration_number)\n self.iteration_stack.push(iteration_number)\n end\n \n # Pop the current iteration context from the stack\n # Called when a sequence finishes repeating\n def pop_iteration_context()\n if size(self.iteration_stack) > 0\n return self.iteration_stack.pop()\n end\n return nil\n end\n \n # Update the current iteration number in the top context\n # Called when a sequence advances to the next iteration\n #\n # @param iteration_number: int - The new iteration number (0-based)\n def update_current_iteration(iteration_number)\n if size(self.iteration_stack) > 0\n self.iteration_stack[-1] = iteration_number\n end\n end\n \n # Get the current iteration number from the innermost sequence context\n # Used by iteration_number to return the current iteration\n #\n # @return int|nil - Current iteration number (0-based) or nil if not in sequence\n def get_current_iteration_number()\n if size(self.iteration_stack) > 0\n return self.iteration_stack[-1]\n end\n return nil\n end\n \nend\n\nreturn {'engine_proxy': engine_proxy}\n"; modules["core/event_handler.be"] = "# Event Handler System for Berry Animation Framework\n# Manages event callbacks and execution\n\nclass EventHandler\n var event_name # Name of the event (e.g., \"button_press\", \"timer\")\n var callback_func # Function to call when event occurs\n var condition # Optional condition function (returns true/false)\n var priority # Handler priority (higher = executed first)\n var is_active # Whether this handler is currently active\n var metadata # Additional event metadata (e.g., timer interval)\n \n def init(event_name, callback_func, priority, condition, metadata)\n self.event_name = event_name\n self.callback_func = callback_func\n self.priority = priority != nil ? priority : 0\n self.condition = condition\n self.is_active = true\n self.metadata = metadata != nil ? metadata : {}\n end\n \n # Execute the event handler if conditions are met\n def execute(event_data)\n if !self.is_active\n return false\n end\n \n # Check condition if provided\n if self.condition != nil\n if !self.condition(event_data)\n return false\n end\n end\n \n # Execute callback\n if self.callback_func != nil\n self.callback_func(event_data)\n return true\n end\n \n return false\n end\n \n # Enable/disable the handler\n def set_active(active)\n self.is_active = active\n end\n \n # Get handler info for debugging\n # def get_info()\n # return {\n # \"event_name\": self.event_name,\n # \"priority\": self.priority,\n # \"is_active\": self.is_active,\n # \"has_condition\": self.condition != nil,\n # \"metadata\": self.metadata\n # }\n # end\nend\n\nclass EventManager\n var handlers # Map of event_name -> list of handlers\n var global_handlers # Handlers that respond to all events\n var event_queue # Simple event queue for deferred processing\n var is_processing # Flag to prevent recursive event processing\n \n def init()\n self.handlers = {}\n self.global_handlers = []\n self.event_queue = []\n self.is_processing = false\n end\n \n # Register an event handler\n def register_handler(event_name, callback_func, priority, condition, metadata)\n var handler = animation.event_handler(event_name, callback_func, priority, condition, metadata)\n \n if event_name == \"*\"\n # Global handler for all events\n self.global_handlers.push(handler)\n self._sort_handlers(self.global_handlers)\n else\n # Specific event handler\n if !self.handlers.contains(event_name)\n self.handlers[event_name] = []\n end\n self.handlers[event_name].push(handler)\n self._sort_handlers(self.handlers[event_name])\n end\n \n return handler\n end\n \n # Remove an event handler\n def unregister_handler(handler)\n if handler.event_name == \"*\"\n var idx = self.global_handlers.find(handler)\n if idx != nil\n self.global_handlers.remove(idx)\n end\n else\n var event_handlers = self.handlers.find(handler.event_name)\n if event_handlers != nil\n var idx = event_handlers.find(handler)\n if idx != nil\n event_handlers.remove(idx)\n end\n end\n end\n end\n \n # Trigger an event immediately\n def trigger_event(event_name, event_data)\n if self.is_processing\n # Queue event to prevent recursion\n self.event_queue.push({\"name\": event_name, \"data\": event_data})\n return\n end\n \n self.is_processing = true\n \n try\n # Execute global handlers first\n for handler : self.global_handlers\n if handler.is_active\n handler.execute({\"event_name\": event_name, \"data\": event_data})\n end\n end\n \n # Execute specific event handlers\n var event_handlers = self.handlers.find(event_name)\n if event_handlers != nil\n for handler : event_handlers\n if handler.is_active\n handler.execute(event_data)\n end\n end\n end\n \n except .. as e, msg\n print(\"Event processing error:\", e, msg)\n end\n \n self.is_processing = false\n \n # Process queued events\n self._process_queued_events()\n end\n \n # Process any queued events\n def _process_queued_events()\n while self.event_queue.size() > 0\n var queued_event = self.event_queue.pop(0)\n self.trigger_event(queued_event[\"name\"], queued_event[\"data\"])\n end\n end\n \n # Sort handlers by priority (higher priority first)\n def _sort_handlers(handler_list)\n # Insertion sort for small lists (embedded-friendly and efficient)\n for i : 1..size(handler_list)-1\n var k = handler_list[i]\n var j = i\n while (j > 0) && (handler_list[j-1].priority < k.priority)\n handler_list[j] = handler_list[j-1]\n j -= 1\n end\n handler_list[j] = k\n end\n end\n \n # Get all registered events\n def get_registered_events()\n var events = []\n for event_name : self.handlers.keys()\n events.push(event_name)\n end\n return events\n end\n \n # Get handlers for a specific event\n def get_handlers(event_name)\n var result = []\n \n # Add global handlers\n for handler : self.global_handlers\n result.push(handler.get_info())\n end\n \n # Add specific handlers\n var event_handlers = self.handlers.find(event_name)\n if event_handlers != nil\n for handler : event_handlers\n result.push(handler.get_info())\n end\n end\n \n return result\n end\n \n # Clear all handlers\n def clear_all_handlers()\n self.handlers.clear()\n self.global_handlers.clear()\n self.event_queue.clear()\n end\n \n # Enable/disable all handlers for an event\n def set_event_active(event_name, active)\n var event_handlers = self.handlers.find(event_name)\n if event_handlers != nil\n for handler : event_handlers\n handler.set_active(active)\n end\n end\n end\nend\n\n# Event system functions to monad\ndef register_event_handler(event_name, callback_func, priority, condition, metadata)\n return animation.event_manager.register_handler(event_name, callback_func, priority, condition, metadata)\nend\n\ndef unregister_event_handler(handler)\n animation.event_manager.unregister_handler(handler)\nend\n\ndef trigger_event(event_name, event_data)\n animation.event_manager.trigger_event(event_name, event_data)\nend\n\ndef get_registered_events()\n return animation.event_manager.get_registered_events()\nend\n\ndef get_event_handlers(event_name)\n return animation.event_manager.get_handlers(event_name)\nend\n\ndef clear_all_event_handlers()\n animation.event_manager.clear_all_handlers()\nend\n\ndef set_event_active(event_name, active)\n animation.event_manager.set_event_active(event_name, active)\nend\n\n# Export classes\nreturn {\n \"event_handler\": EventHandler,\n \"EventManager\": EventManager,\n 'register_event_handler': register_event_handler,\n 'unregister_event_handler': unregister_event_handler,\n 'trigger_event': trigger_event,\n 'get_registered_events': get_registered_events,\n 'get_event_handlers': get_event_handlers,\n 'clear_all_event_handlers': clear_all_event_handlers,\n 'set_event_active': set_event_active,\n}"; modules["core/frame_buffer.be"] = "# frame_buffer class for Berry Animation Framework\n#\n# This class provides a buffer for storing and manipulating pixel data\n# for LED animations. It uses a bytes object for efficient storage and\n# provides methods for pixel manipulation.\n#\n# Each pixel is stored as a 32-bit value (ARGB format - 0xAARRGGBB):\n# - 8 bits for Alpha (0-255, where 0 is fully transparent and 255 is fully opaque)\n# - 8 bits for Red (0-255)\n# - 8 bits for Green (0-255)\n# - 8 bits for Blue (0-255)\n#\n# The class is optimized for performance and minimal memory usage.\n\n# Special import for FrameBufferNtv that is pure Berry but will be replaced\n# by native code in Tasmota, so we don't register to 'animation' module\n# so that it is not solidified\nimport \"./core/frame_buffer_ntv\" as FrameBufferNtv\n\nclass frame_buffer : FrameBufferNtv\n var pixels # Pixel data (bytes object)\n var width # Number of pixels\n \n # Initialize a new frame buffer with the specified width\n # Takes either an int (width) or an instance of frame_buffer (instance)\n def init(width_or_buffer)\n if type(width_or_buffer) == 'int'\n var width = width_or_buffer\n if width <= 0\n raise \"value_error\", \"width must be positive\"\n end\n \n self.width = width\n # Each pixel uses 4 bytes (ARGB), so allocate width * 4 bytes\n # Initialize with zeros to ensure correct size\n var buffer = bytes(width * 4)\n buffer.resize(width * 4)\n self.pixels = buffer\n self.clear() # Initialize all pixels to transparent black\n elif type(width_or_buffer) == 'instance'\n self.width = width_or_buffer.width\n self.pixels = width_or_buffer.pixels.copy()\n else\n raise \"value_error\", \"argument must be either int or instance\"\n end\n end\n \n # Get the pixel color at the specified index\n # Returns the pixel value as a 32-bit integer (ARGB format - 0xAARRGGBB)\n def get_pixel_color(index)\n if index < 0 || index >= self.width\n raise \"index_error\", \"pixel index out of range\"\n end\n \n # Each pixel is 4 bytes, so the offset is index * 4\n return self.pixels.get(index * 4, 4)\n end\n \n # Set the pixel at the specified index with a 32-bit color value\n # color: 32-bit color value in ARGB format (0xAARRGGBB)\n def set_pixel_color(index, color)\n if index < 0 || index >= self.width\n raise \"index_error\", \"pixel index out of range\"\n end\n \n # Set the pixel in the buffer\n self.pixels.set(index * 4, color, 4)\n end\n\n # Clear the frame buffer (set all pixels to transparent black)\n def clear()\n self.pixels.clear() # clear buffer\n if (size(self.pixels) != self.width * 4)\n self.pixels.resize(self.width * 4) # resize to full size filled with transparent black (all zeroes)\n end\n end\n \n # Resize the frame buffer to a new width\n # This is more efficient than creating a new frame buffer object\n def resize(new_width)\n if new_width <= 0\n raise \"value_error\", \"width must be positive\"\n end\n \n if new_width == self.width\n return # No change needed\n end\n \n self.width = new_width\n # Resize the underlying bytes buffer\n self.pixels.resize(self.width * 4)\n # Clear to ensure all new pixels are transparent black\n self.clear()\n end\n \n # # Convert separate a, r, g, b components to a 32-bit color value\n # # r: red component (0-255)\n # # g: green component (0-255)\n # # b: blue component (0-255)\n # # a: alpha component (0-255, default 255 = fully opaque)\n # # Returns: 32-bit color value in ARGB format (0xAARRGGBB)\n # static def to_color(r, g, b, a)\n # # Default alpha to fully opaque if not specified\n # if a == nil\n # a = 255\n # end\n \n # # Ensure values are in valid range\n # r = r & 0xFF\n # g = g & 0xFF\n # b = b & 0xFF\n # a = a & 0xFF\n \n # # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n # return (a << 24) | (r << 16) | (g << 8) | b\n # end\n \n # Convert the frame buffer to a hexadecimal string (for debugging)\n def tohex()\n return self.pixels.tohex()\n end\n \n # Support for array-like access using []\n def item(i)\n return self.get_pixel_color(i)\n end\n \n # Support for array-like assignment using []=\n def setitem(i, v)\n # Use the set_pixel_color method directly with the 32-bit value\n self.set_pixel_color(i, v)\n end\n \n # Create a copy of this frame buffer\n def copy()\n return animation.frame_buffer(self) # return using the self copying constructor\n end\nend\n\nreturn {'frame_buffer': frame_buffer}"; modules["core/frame_buffer_ntv.be"] = "# FrameBuffeNtv class for Berry Animation Framework\n#\n# This class provides a place-holder for native implementation of some\n# static methods.\n#\n# Below is a pure Berry implementation for emulator, while it is replaced\n# by C++ code in Tasmota devices\n\nclass FrameBufferNtv\n\n # Blend two colors using their alpha channels\n # Returns the blended color as a 32-bit integer (ARGB format - 0xAARRGGBB)\n # color1: destination color (ARGB format - 0xAARRGGBB)\n # color2: source color (ARGB format - 0xAARRGGBB)\n static def blend(color1, color2)\n \n # Extract components from color1 (ARGB format - 0xAARRGGBB)\n var a1 = (color1 >> 24) & 0xFF\n var r1 = (color1 >> 16) & 0xFF\n var g1 = (color1 >> 8) & 0xFF\n var b1 = color1 & 0xFF\n \n # Extract components from color2 (ARGB format - 0xAARRGGBB)\n var a2 = (color2 >> 24) & 0xFF\n var r2 = (color2 >> 16) & 0xFF\n var g2 = (color2 >> 8) & 0xFF\n var b2 = color2 & 0xFF\n \n # Fast path for common cases\n if a2 == 0\n # Source is fully transparent, no blending needed\n return color1\n end\n \n # Use the source alpha directly for blending\n var effective_opacity = a2\n \n # Normal alpha blending\n # Use tasmota.scale_uint for ratio conversion instead of integer arithmetic\n var r = tasmota.scale_uint(255 - effective_opacity, 0, 255, 0, r1) + tasmota.scale_uint(effective_opacity, 0, 255, 0, r2)\n var g = tasmota.scale_uint(255 - effective_opacity, 0, 255, 0, g1) + tasmota.scale_uint(effective_opacity, 0, 255, 0, g2)\n var b = tasmota.scale_uint(255 - effective_opacity, 0, 255, 0, b1) + tasmota.scale_uint(effective_opacity, 0, 255, 0, b2)\n \n # More accurate alpha blending using tasmota.scale_uint\n var a = a1 + tasmota.scale_uint((255 - a1) * a2, 0, 255 * 255, 0, 255)\n \n # Ensure values are in valid range\n r = r < 0 ? 0 : (r > 255 ? 255 : r)\n g = g < 0 ? 0 : (g > 255 ? 255 : g)\n b = b < 0 ? 0 : (b > 255 ? 255 : b)\n a = a < 0 ? 0 : (a > 255 ? 255 : a)\n \n # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n return (int(a) << 24) | (int(r) << 16) | (int(g) << 8) | int(b)\n end\n\n # Linear interpolation between two colors using explicit blend factor\n # Returns the blended color as a 32-bit integer (ARGB format - 0xAARRGGBB)\n # \n # This function matches the original berry_animate frame.blend(color1, color2, blend_factor) behavior\n # Used for creating smooth gradients like beacon slew regions\n #\n # color1: destination/background color (ARGB format - 0xAARRGGBB)\n # color2: source/foreground color (ARGB format - 0xAARRGGBB)\n # blend_factor: blend factor (0-255 integer)\n # - 0 = full color2 (foreground)\n # - 255 = full color1 (background)\n static def blend_linear(color1, color2, blend_factor)\n # Extract components from color1 (background/destination)\n var back_a = (color1 >> 24) & 0xFF\n var back_r = (color1 >> 16) & 0xFF\n var back_g = (color1 >> 8) & 0xFF\n var back_b = color1 & 0xFF\n \n # Extract components from color2 (foreground/source)\n var fore_a = (color2 >> 24) & 0xFF\n var fore_r = (color2 >> 16) & 0xFF\n var fore_g = (color2 >> 8) & 0xFF\n var fore_b = color2 & 0xFF\n \n # Linear interpolation using tasmota.scale_uint instead of integer mul/div\n # Maps blend_factor (0-255) to interpolate between fore and back colors\n var result_a = tasmota.scale_uint(blend_factor, 0, 255, fore_a, back_a)\n var result_r = tasmota.scale_uint(blend_factor, 0, 255, fore_r, back_r)\n var result_g = tasmota.scale_uint(blend_factor, 0, 255, fore_g, back_g)\n var result_b = tasmota.scale_uint(blend_factor, 0, 255, fore_b, back_b)\n \n # Combine components into a 32-bit value (ARGB format)\n return (int(result_a) << 24) | (int(result_r) << 16) | (int(result_g) << 8) | int(result_b)\n end\n \n # Fill a region of the buffer with a specific color\n # pixels: destination bytes buffer\n # color: the color to fill (ARGB format - 0xAARRGGBB)\n # start_pos: start position (default: 0)\n # end_pos: end position excluded (default: -1 = last pixel)\n static def fill_pixels(pixels, color, start_pos, end_pos)\n # Default parameters\n if (start_pos == nil) start_pos = 0 end\n if (end_pos == nil) end_pos = -1 end\n \n # Validate region bounds\n var width = size(pixels) / 4\n \n # Handle negative indices (Python-style)\n if (start_pos < 0) start_pos += width end\n if (end_pos < 0) end_pos += width + 1 end\n \n # Clamp to valid range\n if (start_pos < 0) start_pos = 0 end\n if (end_pos < 0) end_pos = 0 end\n if (start_pos >= width) return end\n if (end_pos > width) end_pos = width end\n if (end_pos < start_pos) return end\n \n # Fill the region with the color\n var i = start_pos\n while i < end_pos\n pixels.set(i * 4, color, 4)\n i += 1\n end\n end\n \n # Blend destination buffer with source buffer using per-pixel alpha\n # dest_pixels: destination bytes buffer\n # src_pixels: source bytes buffer\n # region_start: start index for blending\n # region_end: end index for blending\n static def blend_pixels(dest_pixels, src_pixels, region_start, region_end)\n # Default parameters\n if (region_start == nil) region_start = 0 end\n if (region_end == nil) region_end = -1 end\n\n # Validate region bounds\n var dest_width = size(dest_pixels) / 4\n var src_width = size(src_pixels) / 4\n if (dest_width < src_width) dest_width = src_width end\n if (src_width < dest_width) src_width = dest_width end\n\n if (region_start < 0) region_start += dest_width end\n if (region_end < 0) region_end += dest_width end\n if (region_start < 0) region_start = 0 end\n if (region_end < 0)region_end = 0 end\n if (region_start >= dest_width) return end\n if (region_end >= dest_width) region_end = dest_width - 1 end\n if (region_end < region_start) return end\n \n # Blend each pixel using the blend function\n var i = region_start\n while i <= region_end\n var color2 = src_pixels.get(i * 4, 4)\n var a2 = (color2 >> 24) & 0xFF\n \n # Only blend if the source pixel has some alpha\n if a2 > 0\n if a2 == 255\n # Fully opaque source pixel, just copy it\n dest_pixels.set(i * 4, color2, 4)\n else\n # Partially transparent source pixel, need to blend\n var color1 = dest_pixels.get(i * 4, 4)\n var blended = _class.blend(color1, color2)\n dest_pixels.set(i * 4, blended, 4)\n end\n end\n \n i += 1\n end\n end\n \n # Create a gradient fill in the buffer\n # pixels: destination bytes buffer\n # color1: start color (ARGB format - 0xAARRGGBB)\n # color2: end color (ARGB format - 0xAARRGGBB)\n # start_pos: start position (default: 0)\n # end_pos: end position (default: -1 = last pixel)\n static def gradient_fill(pixels, color1, color2, start_pos, end_pos)\n # Default parameters\n if (start_pos == nil) start_pos = 0 end\n if (end_pos == nil) end_pos = -1 end\n \n # Validate region bounds\n var width = size(pixels) / 4\n \n # Handle negative indices (Python-style)\n if (start_pos < 0) start_pos += width end\n if (end_pos < 0) end_pos += width end\n \n # Clamp to valid range\n if (start_pos < 0) start_pos = 0 end\n if (end_pos < 0) end_pos = 0 end\n if (start_pos >= width) return end\n if (end_pos >= width) end_pos = width - 1 end\n if (end_pos < start_pos) return end\n \n # Set first pixel directly\n pixels.set(start_pos * 4, color1, 4)\n \n # If only one pixel, we're done\n if start_pos == end_pos\n return\n end\n \n # Set last pixel directly\n pixels.set(end_pos * 4, color2, 4)\n \n # If only two pixels, we're done\n if end_pos - start_pos <= 1\n return\n end\n \n # Extract components from color1 (ARGB format - 0xAARRGGBB)\n var a1 = (color1 >> 24) & 0xFF\n var r1 = (color1 >> 16) & 0xFF\n var g1 = (color1 >> 8) & 0xFF\n var b1 = color1 & 0xFF\n \n # Extract components from color2 (ARGB format - 0xAARRGGBB)\n var a2 = (color2 >> 24) & 0xFF\n var r2 = (color2 >> 16) & 0xFF\n var g2 = (color2 >> 8) & 0xFF\n var b2 = color2 & 0xFF\n \n # Calculate the total number of steps\n var steps = end_pos - start_pos\n \n # Fill the gradient for intermediate pixels\n var i = start_pos + 1\n while (i < end_pos)\n var pos = i - start_pos\n \n # Use tasmota.scale_uint for ratio conversion instead of floating point arithmetic\n var r = tasmota.scale_uint(pos, 0, steps, r1, r2)\n var g = tasmota.scale_uint(pos, 0, steps, g1, g2)\n var b = tasmota.scale_uint(pos, 0, steps, b1, b2)\n var a = tasmota.scale_uint(pos, 0, steps, a1, a2)\n \n # Ensure values are in valid range\n r = r < 0 ? 0 : (r > 255 ? 255 : r)\n g = g < 0 ? 0 : (g > 255 ? 255 : g)\n b = b < 0 ? 0 : (b > 255 ? 255 : b)\n a = a < 0 ? 0 : (a > 255 ? 255 : a)\n \n # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n var color = (a << 24) | (r << 16) | (g << 8) | b\n pixels.set(i * 4, color, 4)\n i += 1\n end\n end\n \n # Blend a specific region with a solid color using the color's alpha channel\n # pixels: destination bytes buffer\n # color: the color to blend (ARGB format - 0xAARRGGBB)\n # start_pos: start position (default: 0)\n # end_pos: end position (default: -1 = last pixel)\n static def blend_color(pixels, color, start_pos, end_pos)\n # Default parameters\n if (start_pos == nil) start_pos = 0 end\n if (end_pos == nil) end_pos = -1 end\n \n # Validate region bounds\n var width = size(pixels) / 4\n \n # Handle negative indices (Python-style)\n if (start_pos < 0) start_pos += width end\n if (end_pos < 0) end_pos += width end\n \n # Clamp to valid range\n if (start_pos < 0) start_pos = 0 end\n if (end_pos < 0) end_pos = 0 end\n if (start_pos >= width) return end\n if (end_pos >= width) end_pos = width - 1 end\n if (end_pos < start_pos) return end\n \n # Extract alpha from color\n var a2 = (color >> 24) & 0xFF\n \n # Only blend if the color has some alpha\n if a2 == 0\n return # Fully transparent, nothing to do\n end\n \n # Blend the pixels in the specified region\n var i = start_pos\n while i <= end_pos\n var color1 = pixels.get(i * 4, 4)\n var blended = _class.blend(color1, color)\n pixels.set(i * 4, blended, 4)\n i += 1\n end\n end\n \n # Apply an opacity adjustment to a region of the buffer\n # pixels: destination bytes buffer\n # opacity: opacity factor (0-511) OR mask_pixels (bytes buffer to use as mask)\n # - Number: 0 is fully transparent, 255 is original, 511 is maximum opaque\n # - bytes(): uses alpha channel as opacity mask\n # start_pos: start position (default: 0)\n # end_pos: end position (default: -1 = last pixel)\n static def apply_opacity(pixels, opacity, start_pos, end_pos)\n if opacity == nil opacity = 255 end\n \n # Default parameters\n if (start_pos == nil) start_pos = 0 end\n if (end_pos == nil) end_pos = -1 end\n \n # Validate region bounds\n var width = size(pixels) / 4\n \n # Handle negative indices (Python-style)\n if (start_pos < 0) start_pos += width end\n if (end_pos < 0) end_pos += width end\n \n # Clamp to valid range\n if (start_pos < 0) start_pos = 0 end\n if (end_pos < 0) end_pos = 0 end\n if (start_pos >= width) return end\n if (end_pos >= width) end_pos = width - 1 end\n if (end_pos < start_pos) return end\n \n # Check if opacity is a bytes buffer (mask mode)\n if isinstance(opacity, bytes)\n # Mask mode: use another buffer as opacity mask\n var mask_pixels = opacity\n var mask_width = size(mask_pixels) / 4\n \n # Validate mask size\n if mask_width < width\n width = mask_width\n end\n if end_pos >= width\n end_pos = width - 1\n end\n \n # Apply mask opacity\n var i = start_pos\n while i <= end_pos\n var color = pixels.get(i * 4, 4)\n var mask_color = mask_pixels.get(i * 4, 4)\n \n # Extract alpha from mask as opacity factor (0-255)\n var mask_opacity = (mask_color >> 24) & 0xFF\n \n # Extract components from color (ARGB format - 0xAARRGGBB)\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n # Apply mask opacity to alpha channel using tasmota.scale_uint\n a = tasmota.scale_uint(mask_opacity, 0, 255, 0, a)\n \n # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n var new_color = (a << 24) | (r << 16) | (g << 8) | b\n \n # Update the pixel\n pixels.set(i * 4, new_color, 4)\n \n i += 1\n end\n else\n # Number mode: uniform opacity adjustment\n var opacity_value = int(opacity == nil ? 255 : opacity)\n \n # Ensure opacity is in valid range (0-511)\n opacity_value = opacity_value < 0 ? 0 : (opacity_value > 511 ? 511 : opacity_value)\n \n # Apply opacity adjustment\n var i = start_pos\n while i <= end_pos\n var color = pixels.get(i * 4, 4)\n \n # Extract components (ARGB format - 0xAARRGGBB)\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n # Adjust alpha using tasmota.scale_uint\n # For opacity 0-255: scale down alpha\n # For opacity 256-511: scale up alpha (but cap at 255)\n if opacity_value <= 255\n a = tasmota.scale_uint(opacity_value, 0, 255, 0, a)\n else\n # Scale up alpha: map 256-511 to 1.0-2.0 multiplier\n a = tasmota.scale_uint(a * opacity_value, 0, 255 * 255, 0, 255)\n a = a > 255 ? 255 : a # Cap at maximum alpha\n end\n \n # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n color = (a << 24) | (r << 16) | (g << 8) | b\n \n # Update the pixel\n pixels.set(i * 4, color, 4)\n \n i += 1\n end\n end\n end\n\n # Apply a brightness adjustment to a region of the buffer\n # pixels: destination bytes buffer\n # brightness: brightness factor (0-511) OR mask_pixels (bytes buffer to use as mask)\n # - Number: 0 is black, 255 is original, 511 is maximum bright\n # - bytes(): uses alpha channel as brightness mask\n # start_pos: start position (default: 0)\n # end_pos: end position (default: -1 = last pixel)\n static def apply_brightness(pixels, brightness, start_pos, end_pos)\n # Default parameters\n if (start_pos == nil) start_pos = 0 end\n if (end_pos == nil) end_pos = -1 end\n \n # Validate region bounds\n var width = size(pixels) / 4\n \n # Handle negative indices (Python-style)\n if (start_pos < 0) start_pos += width end\n if (end_pos < 0) end_pos += width end\n \n # Clamp to valid range\n if (start_pos < 0) start_pos = 0 end\n if (end_pos < 0) end_pos = 0 end\n if (start_pos >= width) return end\n if (end_pos >= width) end_pos = width - 1 end\n if (end_pos < start_pos) return end\n \n # Check if brightness is a bytes buffer (mask mode)\n if isinstance(brightness, bytes)\n # Mask mode: use another buffer as brightness mask\n var mask_pixels = brightness\n var mask_width = size(mask_pixels) / 4\n \n # Validate mask size\n if mask_width < width\n width = mask_width\n end\n if end_pos >= width\n end_pos = width - 1\n end\n \n # Apply mask brightness\n var i = start_pos\n while i <= end_pos\n var color = pixels.get(i * 4, 4)\n var mask_color = mask_pixels.get(i * 4, 4)\n \n # Extract alpha from mask as brightness factor (0-255)\n var mask_brightness = (mask_color >> 24) & 0xFF\n \n # Extract components from color (ARGB format - 0xAARRGGBB)\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n # Apply mask brightness to RGB channels using tasmota.scale_uint\n r = tasmota.scale_uint(mask_brightness, 0, 255, 0, r)\n g = tasmota.scale_uint(mask_brightness, 0, 255, 0, g)\n b = tasmota.scale_uint(mask_brightness, 0, 255, 0, b)\n \n # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n var new_color = (a << 24) | (r << 16) | (g << 8) | b\n \n # Update the pixel\n pixels.set(i * 4, new_color, 4)\n \n i += 1\n end\n else\n # Number mode: uniform brightness adjustment\n var brightness_value = int(brightness == nil ? 255 : brightness)\n \n # Ensure brightness is in valid range (0-511)\n brightness_value = brightness_value < 0 ? 0 : (brightness_value > 511 ? 511 : brightness_value)\n \n # Apply brightness adjustment\n var i = start_pos\n while i <= end_pos\n var color = pixels.get(i * 4, 4)\n \n # Extract components (ARGB format - 0xAARRGGBB)\n var a = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n # Adjust brightness using tasmota.scale_uint\n # For brightness 0-255: scale down RGB\n # For brightness 256-511: scale up RGB (but cap at 255)\n if brightness_value <= 255\n r = tasmota.scale_uint(r, 0, 255, 0, brightness_value)\n g = tasmota.scale_uint(g, 0, 255, 0, brightness_value)\n b = tasmota.scale_uint(b, 0, 255, 0, brightness_value)\n else\n # Scale up RGB: map 256-511 to 1.0-2.0 multiplier\n var multiplier = brightness_value - 255 # 0-256 range\n r = r + tasmota.scale_uint(r * multiplier, 0, 255 * 256, 0, 255)\n g = g + tasmota.scale_uint(g * multiplier, 0, 255 * 256, 0, 255)\n b = b + tasmota.scale_uint(b * multiplier, 0, 255 * 256, 0, 255)\n r = r > 255 ? 255 : r # Cap at maximum\n g = g > 255 ? 255 : g # Cap at maximum\n b = b > 255 ? 255 : b # Cap at maximum\n end\n \n # Combine components into a 32-bit value (ARGB format - 0xAARRGGBB)\n color = (a << 24) | (r << 16) | (g << 8) | b\n \n # Update the pixel\n pixels.set(i * 4, color, 4)\n \n i += 1\n end\n end\n end\nend\n\nreturn FrameBufferNtv"; modules["core/math_functions.be"] = "# Mathematical Functions for Animation Framework\n#\n# This module provides mathematical functions that can be used in closures\n# and throughout the animation framework. These functions are optimized for\n# the animation use case and handle integer ranges appropriately.\n\n# This class contains only static functions\nclass AnimationMath\n # Minimum of two or more values\n #\n # @param *args: number - Values to compare\n # @return number - Minimum value\n static def min(*args)\n import math\n return call(math.min, args)\n end\n\n # Maximum of two or more values\n #\n # @param *args: number - Values to compare\n # @return number - Maximum value\n static def max(*args)\n import math\n return call(math.max, args)\n end\n\n # Absolute value\n #\n # @param x: number - Input value\n # @return number - Absolute value\n static def abs(x)\n import math\n return math.abs(x)\n end\n\n # Round to nearest integer\n #\n # @param x: number - Input value\n # @return int - Rounded value\n static def round(x)\n import math\n return int(math.round(x))\n end\n\n # Square root with integer handling\n # For integers, treats 1.0 as 255 (full scale)\n #\n # @param x: number - Input value\n # @return number - Square root\n static def sqrt(x)\n import math\n # If x is an integer in 0-255 range, scale to 0-1 for sqrt, then back\n if type(x) == 'int' && x >= 0 && x <= 255\n var normalized = x / 255.0\n return int(math.sqrt(normalized) * 255)\n else\n return math.sqrt(x)\n end\n end\n\n # Scale a value from one range to another using tasmota.scale_int\n #\n # @param v: number - Value to scale\n # @param from_min: number - Source range minimum\n # @param from_max: number - Source range maximum\n # @param to_min: number - Target range minimum\n # @param to_max: number - Target range maximum\n # @return int - Scaled value\n static def scale(v, from_min, from_max, to_min, to_max)\n return tasmota.scale_int(v, from_min, from_max, to_min, to_max)\n end\n\n # Sine function using tasmota.sine_int (works on integers)\n # Input angle is in 0-255 range (mapped to 0-360 degrees)\n # Output is in -255 to 255 range (mapped from -1.0 to 1.0)\n #\n # @param angle: number - Angle in 0-255 range (0-360 degrees)\n # @return int - Sine value in -255 to 255 range\n static def sin(angle)\n # Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)\n var tasmota_angle = tasmota.scale_int(angle, 0, 255, 0, 32767)\n \n # Get sine value from -4096 to 4096 (representing -1.0 to 1.0)\n var sine_val = tasmota.sine_int(tasmota_angle)\n \n # Map from -4096..4096 to -255..255 for integer output\n return tasmota.scale_int(sine_val, -4096, 4096, -255, 255)\n end\n\n # Cosine function using tasmota.sine_int with phase shift\n # Input angle is in 0-255 range (mapped to 0-360 degrees)\n # Output is in -255 to 255 range (mapped from -1.0 to 1.0)\n # Note: This matches the oscillator COSINE behavior (starts at minimum, not maximum)\n #\n # @param angle: number - Angle in 0-255 range (0-360 degrees)\n # @return int - Cosine value in -255 to 255 range\n static def cos(angle)\n # Map angle from 0-255 to 0-32767 (tasmota.sine_int input range)\n var tasmota_angle = tasmota.scale_int(angle, 0, 255, 0, 32767)\n \n # Get cosine value by shifting sine by -90 degrees (matches oscillator behavior)\n var cosine_val = tasmota.sine_int(tasmota_angle - 8192)\n \n # Map from -4096..4096 to -255..255 for integer output\n return tasmota.scale_int(cosine_val, -4096, 4096, -255, 255)\n end\nend\n\n# Export only the _math namespace containing all math functions\nreturn {\n '_math': AnimationMath\n}"; modules["core/param_encoder.be"] = "# Parameter Constraint Encoder for Berry Animation Framework\n#\n# This module provides functions to encode parameter constraints into a compact\n# bytes() format with type-prefixed values for maximum flexibility and correctness.\n#\n# Encoding Format:\n# ----------------\n# Byte 0: Constraint mask (bit field)\n# Bit 0 (0x01): has_min\n# Bit 1 (0x02): has_max\n# Bit 2 (0x04): has_default\n# Bit 3 (0x08): has_explicit_type\n# Bit 4 (0x10): has_enum\n# Bit 5 (0x20): is_nillable\n# Bits 6-7: reserved\n#\n# Bytes 1+: Values in order (min, max, default, enum)\n# Each value is prefixed with its own type byte, followed by the value data.\n#\n# Value Type Codes:\n# 0x00 = int8 (1 byte, signed -128 to 127)\n# 0x01 = int16 (2 bytes, signed -32768 to 32767)\n# 0x02 = int32 (4 bytes, signed integer)\n# 0x03 = string (1-byte length prefix + string bytes)\n# 0x04 = bytes (2-byte length prefix + byte data)\n# 0x05 = bool (1 byte, 0 or 1)\n# 0x06 = nil (0 bytes)\n#\n# Value Encoding (each value has: type_byte + data):\n# - min: [type_byte][value_data]\n# - max: [type_byte][value_data]\n# - default: [type_byte][value_data]\n# - enum: [count_byte][type_byte][value_data][type_byte][value_data]...\n# - explicit_type: [type_code] (only if has_explicit_type bit is set)\n#\n# Explicit Type Codes (semantic types for validation) (1 byte):\n# 0x00 = int\n# 0x01 = string\n# 0x02 = bytes\n# 0x03 = bool\n# 0x04 = any\n# 0x05 = instance\n# 0x06 = function\n\n# Encode a full PARAMS map into a map of encoded constraints\n#\n# @param params_map: map - Map of parameter names to constraint definitions\n# @return map - Map of parameter names to encoded bytes() objects\n#\n# Example:\n# animation.enc_params({\"color\": {\"default\": 0xFFFFFFFF}, \"size\": {\"min\": 0, \"max\": 255, \"default\": 128}})\n# => {\"color\": bytes(\"04 02 FFFFFFFF\"), \"size\": bytes(\"07 00 00 FF 80\")}\ndef encode_constraints(params_map)\n # Nested function: Encode a single constraint map into bytes() format\n def encode_single_constraint(constraint_map)\n # Nested helper: Determine the appropriate type code for a value\n def get_type_code(value)\n var value_type = type(value)\n if value == nil return 0x06 #-NIL-#\n elif value_type == \"bool\" return 0x05 #-BOOL-#\n elif value_type == \"string\" return 0x03 #-STRING-#\n elif value_type == \"instance\" && isinstance(value, bytes) return 0x04 #-BYTES-#\n elif value_type == \"int\"\n # Use signed ranges: int8 for -128 to 127, int16 for larger values\n if value >= -128 && value <= 127 return 0x00 #-INT8-#\n elif value >= -32768 && value <= 32767 return 0x01 #-INT16-#\n else return 0x02 #-INT32-# end\n else return 0x02 #-INT32-# end\n end\n \n # Nested helper: Encode a single value with its type prefix\n def encode_value_with_type(value, result)\n var type_code = get_type_code(value)\n result.add(type_code, 1) # Add type byte prefix\n \n if type_code == 0x06 #-NIL-# return\n elif type_code == 0x05 #-BOOL-# result.add(value ? 1 : 0, 1)\n elif type_code == 0x00 #-INT8-# result.add(value & 0xFF, 1)\n elif type_code == 0x01 #-INT16-# result.add(value & 0xFFFF, 2)\n elif type_code == 0x02 #-INT32-# result.add(value, 4)\n elif type_code == 0x03 #-STRING-#\n var str_bytes = bytes().fromstring(value)\n result.add(size(str_bytes), 1)\n result .. str_bytes\n elif type_code == 0x04 #-BYTES-#\n result.add(size(value), 2)\n result .. value\n end\n end\n \n var mask = 0\n var result = bytes()\n \n # Reserve space for mask only (will be set at the end)\n result.resize(1)\n \n # Helper: Convert explicit type string to type code\n def get_explicit_type_code(type_str)\n if type_str == \"int\" return 0x00\n elif type_str == \"string\" return 0x01\n elif type_str == \"bytes\" return 0x02\n elif type_str == \"bool\" return 0x03\n elif type_str == \"any\" return 0x04\n elif type_str == \"instance\" return 0x05\n elif type_str == \"function\" return 0x06\n end\n return 0x04 # Default to \"any\"\n end\n \n # Check if explicit type is specified\n var explicit_type_code = nil\n if constraint_map.contains(\"type\")\n explicit_type_code = get_explicit_type_code(constraint_map[\"type\"])\n end\n \n # Encode min value (with type prefix)\n if constraint_map.contains(\"min\")\n mask |= 0x01 #-HAS_MIN-#\n encode_value_with_type(constraint_map[\"min\"], result)\n end\n \n # Encode max value (with type prefix)\n if constraint_map.contains(\"max\")\n mask |= 0x02 #-HAS_MAX-#\n encode_value_with_type(constraint_map[\"max\"], result)\n end\n \n # Encode default value (with type prefix)\n if constraint_map.contains(\"default\")\n mask |= 0x04 #-HAS_DEFAULT-#\n encode_value_with_type(constraint_map[\"default\"], result)\n end\n \n # Encode explicit type code if present (1 byte)\n if explicit_type_code != nil\n mask |= 0x08 #-HAS_EXPLICIT_TYPE-#\n result.add(explicit_type_code, 1)\n end\n \n # Encode enum values (each with type prefix)\n if constraint_map.contains(\"enum\")\n mask |= 0x10 #-HAS_ENUM-#\n var enum_list = constraint_map[\"enum\"]\n result.add(size(enum_list), 1) # Enum count\n for val : enum_list\n encode_value_with_type(val, result)\n end\n end\n \n # Set nillable flag\n if constraint_map.contains(\"nillable\") && constraint_map[\"nillable\"]\n mask |= 0x20 #-IS_NILLABLE-#\n end\n \n # Write mask at the beginning\n result.set(0, mask, 1)\n \n return result\n end\n \n # Encode each parameter constraint\n var result = {}\n for param_name : params_map.keys()\n result[param_name] = encode_single_constraint(params_map[param_name])\n end\n return result\nend\n\n# # Decode a single value from bytes according to type code\n# #\n# # @param encoded_bytes: bytes - bytes() object to read from\n# # @param offset: int - Offset to start reading from\n# # @param type_code: int - Type code for decoding\n# # @return [value, new_offset] - Decoded value and new offset\n# def decode_value(encoded_bytes, offset, type_code)\n# if type_code == 0x06 #-NIL-#\n# return [nil, offset]\n# elif type_code == 0x05 #-BOOL-#\n# return [encoded_bytes[offset] != 0, offset + 1]\n# elif type_code == 0x00 #-INT8-#\n# var val = encoded_bytes[offset]\n# # Handle signed int8\n# if val > 127\n# val = val - 256\n# end\n# return [val, offset + 1]\n# elif type_code == 0x01 #-INT16-#\n# var val = encoded_bytes.get(offset, 2)\n# # Handle signed int16\n# if val > 32767\n# val = val - 65536\n# end\n# return [val, offset + 2]\n# elif type_code == 0x02 #-INT32-#\n# return [encoded_bytes.get(offset, 4), offset + 4]\n# elif type_code == 0x03 #-STRING-#\n# var length = encoded_bytes[offset]\n# var str_bytes = encoded_bytes[offset + 1 .. offset + length]\n# return [str_bytes.asstring(), offset + 1 + length]\n# elif type_code == 0x04 #-BYTES-#\n# var length = encoded_bytes.get(offset, 2)\n# var byte_data = encoded_bytes[offset + 2 .. offset + 2 + length - 1]\n# return [byte_data, offset + 2 + length]\n# end\n# \n# return [nil, offset]\n# end\n\n# # Decode an encoded constraint bytes() back into a map\n# #\n# # @param encoded_bytes: bytes - Encoded constraint as bytes() object\n# # @return map - Decoded constraint map\n# #\n# # Example:\n# # decode_constraint(bytes(\"07 00 00 FF 80\"))\n# # => {\"min\": 0, \"max\": 255, \"default\": 128}\n# def decode_constraint(encoded_bytes)\n# if size(encoded_bytes) < 2\n# return {}\n# end\n# \n# var mask = encoded_bytes[0]\n# var type_code = encoded_bytes[1]\n# var offset = 2\n# var result = {}\n# \n# # Decode min value\n# if mask & 0x01 #-HAS_MIN-#\n# var decoded = decode_value(encoded_bytes, offset, type_code)\n# result[\"min\"] = decoded[0]\n# offset = decoded[1]\n# end\n# \n# # Decode max value\n# if mask & 0x02 #-HAS_MAX-#\n# var decoded = decode_value(encoded_bytes, offset, type_code)\n# result[\"max\"] = decoded[0]\n# offset = decoded[1]\n# end\n# \n# # Decode default value\n# if mask & 0x04 #-HAS_DEFAULT-#\n# var decoded = decode_value(encoded_bytes, offset, type_code)\n# result[\"default\"] = decoded[0]\n# offset = decoded[1]\n# end\n# \n# # Decode enum values\n# if mask & 0x10 #-HAS_ENUM-#\n# var count = encoded_bytes[offset]\n# offset += 1\n# result[\"enum\"] = []\n# var i = 0\n# while i < count\n# var decoded = decode_value(encoded_bytes, offset, type_code)\n# result[\"enum\"].push(decoded[0])\n# offset = decoded[1]\n# i += 1\n# end\n# end\n# \n# # Set nillable flag\n# if mask & 0x20 #-IS_NILLABLE-#\n# result[\"nillable\"] = true\n# end\n# \n# # Add type annotation if not default int32\n# if type_code == 0x03 #-STRING-#\n# result[\"type\"] = \"string\"\n# elif type_code == 0x04 #-BYTES-#\n# result[\"type\"] = \"bytes\"\n# elif type_code == 0x05 #-BOOL-#\n# result[\"type\"] = \"bool\"\n# elif type_code == 0x06 #-NIL-#\n# result[\"type\"] = \"nil\"\n# end\n# \n# return result\n# end\n\n# Export only the encode function (decode not needed - use constraint_mask/constraint_find instead)\n# Note: constraint_mask() and constraint_find() are static methods\n# in parameterized_object class for accessing encoded constraints\nreturn {\n 'enc_params': encode_constraints\n}\n"; modules["core/parameterized_object.be"] = "# parameterized_object - Base class for parameter management and playable behavior\n#\n# This class provides a common parameter management system that can be shared\n# between Animation and value_provider classes. It handles parameter validation,\n# storage, and retrieval with support for value_provider instances.\n#\n# It also provides the common interface for playable objects (animations and sequences)\n# that can be started, stopped, and updated over time. This enables:\n# - Unified engine management (single list instead of separate lists)\n# - Hybrid objects that combine rendering and orchestration\n# - Consistent lifecycle management (start/stop/update)\n#\n# Parameters are stored in a 'values' map and accessed via virtual instance variables\n# through member() and setmember() methods. Subclasses should not declare instance\n# variables for parameters, but use the PARAMS system only.\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass parameterized_object\n var values # Map storing all parameter values\n var engine # Reference to the animation engine\n var start_time # Time when object started (ms) (int), value is set at first call to update() or render()\n var is_running # Whether the object is active\n static var VALUE_PROVIDER = false # Set to true in value_provider subclasses\n \n # Produce a value for a specific parameter name and time\n # This is the main method that value_provider subclasses should override\n #\n # `name` argument is generally ignored and the same value\n # is returned for any name, however this allows to have\n # special value providers that return coordinated distinct\n # values for different parameter names.\n #\n # For value providers, start is typically not called because instances\n # can be embedded in closures. So value providers must consider the first\n # call to `produce_value()` as a start of their internal time reference.\n #\n # @param name: string - Parameter name being requested\n # @param time_ms: int - Current time in milliseconds\n # @return any - Value appropriate for the parameter type\n def produce_value(name, time_ms)\n return module(\"undefined\") # Default behavior - return undefined\n end\n\n # Initialize parameter system\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n if engine == nil || type(engine) != \"instance\"\n raise \"value_error\", \"missing engine parameter\"\n end\n \n self.engine = engine\n self.values = {}\n self.is_running = false\n self._init_parameter_values()\n end\n \n # Private method to initialize parameter values from the class hierarchy\n def _init_parameter_values()\n import introspect\n \n # Walk up the class hierarchy to initialize parameters with defaults\n var current_class = classof(self)\n while current_class != nil\n # Check if this class has PARAMS\n if introspect.contains(current_class, \"PARAMS\")\n var class_params = current_class.PARAMS\n # Initialize parameters from this class with their default values\n for param_name : class_params.keys()\n # Only set if not already set (child class defaults take precedence)\n if !self.values.contains(param_name)\n var encoded_constraints = class_params[param_name]\n # Use static method to check for default value\n if self.constraint_mask(encoded_constraints, \"default\")\n self.values[param_name] = self.constraint_find(encoded_constraints, \"default\")\n end\n end\n end\n end\n \n # Move to parent class\n current_class = super(current_class)\n end\n end\n \n # Private method to check if a parameter exists in the class hierarchy\n #\n # @param name: string - Parameter name to check\n # @return bool - True if parameter exists in any class in the hierarchy\n def has_param(name)\n return (self._get_param_def(name) != nil)\n end\n \n # Private method to get parameter definition from the class hierarchy\n #\n # @param name: string - Parameter name\n # @return bytes - Encoded parameter constraints or nil if not found\n def _get_param_def(name)\n import introspect\n \n # Walk up the class hierarchy to find the parameter definition\n var current_class = classof(self)\n while current_class != nil\n # Check if this class has PARAMS\n if introspect.contains(current_class, \"PARAMS\")\n var class_params = current_class.PARAMS\n if class_params.contains(name)\n return class_params[name] # Returns encoded bytes\n end\n end\n \n # Move to parent class\n current_class = super(current_class)\n end\n \n return nil\n end\n \n # Virtual member access - allows obj.param_name syntax\n # This is called when accessing a member that doesn't exist as a real instance variable\n #\n # @param name: string - Parameter name being accessed\n # @return any - Resolved parameter value (value_provider resolved to actual value)\n def member(name)\n # if global.debug_animation\n # log(f\">>> member {name=}\", 3)\n # end\n # Check if it's a parameter (either set in values or defined in PARAMS)\n # Implement a fast-track if the value exists\n\n # main case, the value is numerical and present, so `find()` will get it in one search\n var value = self.values.find(name)\n if (value != nil) # in not nil, there is a value\n if type(value) != \"instance\"\n return value\n end\n return self.resolve_value(value, name, self.engine.time_ms)\n elif self.values.contains(name) # second case, nil is the actual value (and not returned because not found)\n return nil\n else\n # Return default if available from class hierarchy\n var encoded_constraints = self._get_param_def(name)\n if encoded_constraints != nil\n if self.constraint_mask(encoded_constraints, \"default\")\n return self.constraint_find(encoded_constraints, \"default\")\n else\n return nil\n end\n else\n raise \"attribute_error\", f\"'{classname(self)}' object has no attribute '{name}'\"\n end\n end\n end\n \n # Virtual member assignment - allows obj.param_name = value syntax\n # This is called when setting a member that doesn't exist as a real instance variable\n #\n # @param name: string - Parameter name being set\n # @param value: any - Value to set (can be static value or value_provider)\n def setmember(name, value)\n # Check if it's a parameter in the class hierarchy and set it with validation\n if self.has_param(name)\n self._set_parameter_value(name, value)\n else\n # Not a parameter, this will cause an error in normal Berry behavior\n raise \"attribute_error\", f\"'{classname(self)}' object has no attribute '{name}'\"\n end\n end\n \n # Internal method to set a parameter value with validation\n #\n # @param name: string - Parameter name\n # @param value: any - Value to set (can be static value or value_provider)\n def _set_parameter_value(name, value)\n # Validate the value (skip validation for value_provider instances)\n if !animation.is_value_provider(value)\n value = self._validate_param(name, value) # Get potentially converted value\n end\n \n # Store the value\n self.values[name] = value\n \n # Notify of parameter change\n self.on_param_changed(name, value)\n end\n \n # Internal method to resolve a parameter value (handles value_providers)\n #\n # @param name: string - Parameter name\n # @param time_ms: int - Current time in milliseconds for value_provider resolution\n # @return any - Resolved value (static or from value_provider)\n def _resolve_parameter_value(name, time_ms)\n if !self.values.contains(name)\n # Return default if available from class hierarchy\n var encoded_constraints = self._get_param_def(name)\n if encoded_constraints != nil && self.constraint_mask(encoded_constraints, \"default\")\n return self.constraint_find(encoded_constraints, \"default\")\n end\n return nil\n end\n \n var value = self.values[name]\n \n # Apply produce_value() if it' a value_provider\n return self.resolve_value(value, name, time_ms)\n end\n \n # Validate a parameter value against its constraints\n # Raises detailed exceptions for validation failures\n #\n # @param name: string - Parameter name\n # @param value: any - Value to validate (may be modified for real->int conversion)\n # @return any - Validated value (potentially converted from real to int)\n def _validate_param(name, value)\n var encoded_constraints = self._get_param_def(name)\n if encoded_constraints == nil\n raise \"attribute_error\", f\"'{classname(self)}' object has no attribute '{name}'\"\n end\n \n # Accept value_provider instances for all parameters\n if animation.is_value_provider(value)\n return value\n end\n \n # Handle nil values\n if value == nil\n # Check if nil is explicitly allowed via nillable attribute\n if self.constraint_mask(encoded_constraints, \"nillable\")\n return value # nil is allowed for this parameter\n end\n \n # Check if there's a default value (nil is acceptable if there's a default)\n if self.constraint_mask(encoded_constraints, \"default\")\n return self.constraint_find(encoded_constraints, \"default\") # nil is not allowed, use default\n end\n \n # nil is not allowed for this parameter\n raise \"value_error\", f\"'{name}' does not accept nil values\"\n end\n \n # Type validation - default type is \"int\" if not specified\n var expected_type = self.constraint_find(encoded_constraints, \"type\", \"int\")\n \n # Normalize type synonyms to their base types\n # 'time', 'percentage', 'color' are synonyms for 'int'\n # 'palette' is synonym for 'bytes'\n if expected_type == \"time\" || expected_type == \"percentage\" || expected_type == \"color\"\n expected_type = \"int\"\n elif expected_type == \"palette\"\n expected_type = \"bytes\"\n end\n \n # Get actual type for validation\n var actual_type = type(value)\n \n # Skip type validation if expected type is \"any\"\n if expected_type != \"any\"\n # Special case: accept real values for int parameters and convert them\n if expected_type == \"int\" && actual_type == \"real\"\n import math\n value = int(math.round(value))\n actual_type = \"int\"\n # Special case: check for bytes type using isinstance()\n elif expected_type == \"bytes\"\n if actual_type == \"instance\" && isinstance(value, bytes)\n actual_type = \"bytes\"\n elif actual_type != \"instance\" || !isinstance(value, bytes)\n raise \"value_error\", f\"'{name}' expects type '{expected_type}' but got '{actual_type}' (value: {value})\"\n end\n elif expected_type != actual_type\n raise \"value_error\", f\"'{name}' expects type '{expected_type}' but got '{actual_type}' (value: {value})\"\n end\n end\n \n # Range validation for integer values only\n if actual_type == \"int\"\n if self.constraint_mask(encoded_constraints, \"min\")\n var min_val = self.constraint_find(encoded_constraints, \"min\")\n if value < min_val\n raise \"value_error\", f\"'{name}' value {value} is below minimum {min_val}\"\n end\n end\n if self.constraint_mask(encoded_constraints, \"max\")\n var max_val = self.constraint_find(encoded_constraints, \"max\")\n if value > max_val\n raise \"value_error\", f\"'{name}' value {value} is above maximum {max_val}\"\n end\n end\n end\n \n # Enum validation\n if self.constraint_mask(encoded_constraints, \"enum\")\n var valid = false\n var enum_list = self.constraint_find(encoded_constraints, \"enum\")\n var list_size = size(enum_list)\n var i = 0\n while (i < list_size)\n var enum_value = enum_list[i]\n if value == enum_value\n valid = true\n break\n end\n i += 1\n end\n if !valid\n raise \"value_error\", f\"'{name}' value {value} is not in allowed values {enum_list}\"\n end\n end\n \n return value\n end\n \n # Set a parameter value with validation\n #\n # @param name: string - Parameter name\n # @param value: any - Value to set\n # @return bool - True if parameter was set, false if validation failed\n def set_param(name, value)\n # Check if parameter exists in class hierarchy\n if !self.has_param(name)\n return false\n end\n \n try\n self._set_parameter_value(name, value)\n return true\n except \"value_error\" as e\n # Validation failed - return false for method-based setting\n return false\n end\n end\n \n # Get a parameter value (returns raw stored value, not resolved)\n #\n # @param name: string - Parameter name\n # @param default_value: any - Default value if parameter not found\n # @return any - Parameter value or default (may be value_provider)\n def get_param(name, default_value)\n # Check stored values\n if self.values.contains(name)\n return self.values[name]\n end\n \n # Fall back to parameter default from class hierarchy\n var encoded_constraints = self._get_param_def(name)\n if encoded_constraints != nil && self.constraint_mask(encoded_constraints, \"default\")\n return self.constraint_find(encoded_constraints, \"default\", default_value)\n end\n \n return default_value\n end\n \n # Helper method to resolve a value that can be either static or from a value provider\n #\n # @param value: any - Static value or value provider instance\n # @param param_name: string - Parameter name for specific produce_value() method lookup\n # @param time_ms: int - Current time in milliseconds\n # @return any - The resolved value (static or from provider)\n def resolve_value(value, name, time_ms)\n if animation.is_value_provider(value) # this also captures 'nil'\n var ret = value.produce_value(name, time_ms)\n\n # If result is `nil` we check if the parameter is nillable, if so use default value\n if (ret == nil)\n var encoded_constraints = self._get_param_def(name)\n if !self.constraint_mask(encoded_constraints, \"nillable\") &&\n self.constraint_mask(encoded_constraints, \"default\")\n\n ret = self.constraint_find(encoded_constraints, \"default\")\n end\n end\n return ret\n else\n return value\n end\n end\n \n # Helper method to get a resolved value from either a static value or a value provider\n # This is the same as accessing obj.param_name but with explicit time\n #\n # @param param_name: string - Name of the parameter\n # @param time_ms: int - Current time in milliseconds\n # @return any - The resolved value (static or from provider)\n def get_param_value(param_name)\n return self.member(param_name)\n end\n \n # Helper function to make sure both self.start_time and time_ms are valid\n #\n # If time_ms is nil, replace with time_ms from engine\n # Then initialize the value for self.start_time if not set already\n #\n # @param time_ms: int or nil - Current time in milliseconds\n # @return time_ms: int (guaranteed)\n def _fix_time_ms(time_ms)\n if time_ms == nil\n time_ms = self.engine.time_ms\n end\n if self.start_time == nil\n self.start_time = time_ms\n end\n return time_ms\n end\n\n # Start the object - base implementation\n #\n # `start(time_ms)` is called whenever an animation is about to be run\n # by the animation engine directly or via a sequence manager.\n # For value providers, start is typically not called because instances\n # can be embedded in closures. So value providers must consider the first\n # call to `produce_value()` as a start of their internal time reference.\n # \n # Subclasses should override this to implement their start behavior.\n #\n # @param time_ms: int - Start time in milliseconds (optional, uses engine time if nil)\n # @return self for method chaining\n def start(time_ms)\n # Use engine time if not provided\n if time_ms == nil\n time_ms = self.engine.time_ms\n end\n \n # Set is_running to true\n self.is_running = true\n \n # Only reset start_time if it was already started (for value providers)\n # Animations override this to always set start_time\n if self.start_time != nil\n self.start_time = time_ms\n end\n \n return self\n end\n \n # Stop the object\n # Subclasses should override this to implement their stop behavior\n #\n # @return self for method chaining\n def stop()\n # Set is_running to false\n self.is_running = false\n return self\n end\n \n # Update object state based on current time\n # Subclasses must override this to implement their update logic\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Default implementation does nothing - subclasses override as needed\n end\n \n # Method called when a parameter is changed\n # Subclasses should override this to handle parameter changes\n #\n # @param name: string - Parameter name\n # @param value: any - New parameter value\n def on_param_changed(name, value)\n end\n \n # Equality operator for object identity comparison\n # This prevents the member() method from being called during == comparisons\n #\n # @param other: any - Object to compare with\n # @return bool - True if objects are the same instance\n def ==(other)\n import introspect\n return introspect.toptr(self) == introspect.toptr(other)\n end\n \n # Default method to convert instance to boolean\n # Having an explicit method prevents from calling member()\n # Always return 'true' to mimick default test of instance existance\n #\n # @return bool - always True since the instance is not 'nil'\n def tobool()\n return true\n end\n\n # Minimal string representation - returns class name only\n # Subclasses no longer override this to reduce code size\n def tostring()\n return f\"\"\n end\n\n # Inequality operator for object identity comparison\n # This prevents the member() method from being called during != comparisons\n #\n # @param other: any - Object to compare with\n # @return bool - True if objects are different instances\n def !=(other)\n return !(self == other)\n end\n \n # ============================================================================\n # STATIC METHODS FOR ENCODED CONSTRAINT ACCESS\n # ============================================================================\n # PARAMETER CONSTRAINT ENCODING\n # ==============================\n #\n # Parameter constraints are encoded into a compact bytes() format for efficient\n # storage and transmission. Each value is prefixed with its own type byte for\n # maximum flexibility and correctness.\n #\n # Byte 0: Constraint mask (bit field)\n # Bit 0 (0x01): has_min\n # Bit 1 (0x02): has_max\n # Bit 2 (0x04): has_default\n # Bit 3 (0x08): has_explicit_type\n # Bit 4 (0x10): has_enum\n # Bit 5 (0x20): is_nillable\n # Bits 6-7: reserved\n #\n # Bytes 1+: Type-prefixed values in order (min, max, default, enum)\n # Each value consists of: [type_byte][value_data]\n #\n # Value Type Codes:\n # 0x00 = int8 (1 byte, signed -128 to 127)\n # 0x01 = int16 (2 bytes, signed -32768 to 32767)\n # 0x02 = int32 (4 bytes, signed integer)\n # 0x03 = string (1-byte length prefix + string bytes)\n # 0x04 = bytes (2-byte length prefix + byte data)\n # 0x05 = bool (1 byte, 0 or 1)\n # 0x06 = nil (0 bytes)\n #\n # Explicit Type Codes (semantic types for validation) (1 byte):\n # 0x00 = int\n # 0x01 = string\n # 0x02 = bytes\n # 0x03 = bool\n # 0x04 = any\n # 0x05 = instance\n # 0x06 = function\n #\n # ENCODING EXAMPLES:\n #\n # {\"min\": 0, \"max\": 255, \"default\": 128}\n # => bytes(\"07 00 00 01 00FF 00 0080\") # 8 bytes\n # Breakdown:\n # 07 = mask (has_min|has_max|has_default)\n # 00 00 = min (type=int8, value=0)\n # 01 00FF = max (type=int16, value=255)\n # 00 0080 = default (type=int8, value=128)\n #\n # {\"enum\": [1, 2, 3], \"default\": 1}\n # => bytes(\"0C 00 01 03 00 01 00 02 00 03\") # 10 bytes\n # Breakdown:\n # 0C = mask (has_enum|has_default)\n # 00 01 = default (type=int8, value=1)\n # 03 = enum count (3 values)\n # 00 01 = enum[0] (type=int8, value=1)\n # 00 02 = enum[1] (type=int8, value=2)\n # 00 03 = enum[2] (type=int8, value=3)\n #\n # {\"default\": nil, \"nillable\": true}\n # => bytes(\"14 06\") # 2 bytes\n # Breakdown:\n # 14 = mask (has_default|is_nillable)\n # 06 = default (type=nil, no value data)\n #\n # USAGE:\n #\n # Encoding constraints (see param_encoder.be):\n # import param_encoder\n # var encoded = param_encoder.animation.enc_params({\"min\": 0, \"max\": 255, \"default\": 128})\n #\n # Checking if constraint contains a field:\n # if parameterized_object.constraint_mask(encoded, \"min\")\n # print(\"Has min constraint\")\n # end\n #\n # Getting constraint field value:\n # var min_val = parameterized_object.constraint_find(encoded, \"min\", 0)\n # var max_val = parameterized_object.constraint_find(encoded, \"max\", 255)\n # ============================================================================\n # Check if an encoded constraint contains a specific field (monolithic, no sub-calls)\n #\n # This static method provides fast access to encoded constraint metadata without\n # decoding the entire constraint. It directly checks the mask byte to determine\n # if a field is present.\n #\n # @param encoded_bytes: bytes - Encoded constraint in Hybrid format\n # @param name: string - Field name (\"min\", \"max\", \"default\", \"enum\", \"nillable\", \"type\")\n # @return bool - True if field exists, false otherwise\n #\n # Example:\n # var encoded = bytes(\"07 00 00 FF 80\") # min=0, max=255, default=128\n # parameterized_object.constraint_mask(encoded, \"min\") # => true\n # parameterized_object.constraint_mask(encoded, \"enum\") # => false\n static var _MASK = [\n \"min\", #- 0x01 HAS_MIN-#\n \"max\", #- 0x02, HAS_MAX-#\n \"default\", #- 0x04, HAS_DEFAULT-#\n \"type\", #- 0x08, HAS_EXPLICIT_TYPE-#\n \"enum\", #- 0x10, HAS_ENUM-#\n \"nillable\", #- 0x20, IS_NILLABLE-#\n ]\n static var _TYPES = [\n \"int\", # 0x00\n \"string\", # 0x01\n \"bytes\", # 0x02\n \"bool\", # 0x03\n \"any\", # 0x04\n \"instance\", # 0x05\n \"function\" # 0x06\n ]\n static def constraint_mask(encoded_bytes, name)\n if (encoded_bytes != nil) && size(encoded_bytes) > 0\n var index_mask = _class._MASK.find(name)\n if (index_mask != nil)\n return (encoded_bytes[0] & (1 << index_mask))\n end\n end\n return 0\n end\n \n # Find and return an encoded constraint field value (monolithic, no sub-calls)\n #\n # This static method extracts a specific field value from an encoded constraint\n # without decoding the entire structure. It performs direct byte reading with\n # inline type handling for maximum efficiency.\n #\n # @param encoded_bytes: bytes - Encoded constraint in Hybrid format\n # @param name: string - Field name (\"min\", \"max\", \"default\", \"enum\", \"nillable\", \"type\")\n # @param default: any - Default value if field not found\n # @return any - Field value or default\n #\n # Supported field names:\n # - \"min\": Minimum value constraint (int)\n # - \"max\": Maximum value constraint (int)\n # - \"default\": Default value (any type)\n # - \"enum\": List of allowed values (array)\n # - \"nillable\": Whether nil is allowed (bool)\n # - \"type\": Explicit type string (\"int\", \"string\", \"bytes\", \"bool\", \"any\", \"instance\", \"function\")\n #\n # Example:\n # var encoded = bytes(\"07 00 00 FF 80\") # min=0, max=255, default=128\n # parameterized_object.constraint_find(encoded, \"min\", 0) # => 0\n # parameterized_object.constraint_find(encoded, \"max\", 255) # => 255\n # parameterized_object.constraint_find(encoded, \"default\", 100) # => 128\n # parameterized_object.constraint_find(encoded, \"enum\", nil) # => nil (not present)\n \n static def constraint_find(encoded_bytes, name, default)\n\n # Helper: Skip a value with type prefix and return new offset\n def _skip_typed_value(encoded_bytes, offset)\n if offset >= size(encoded_bytes) return 0 end\n var type_code = encoded_bytes[offset]\n \n if type_code == 0x06 #-NIL-# return 1\n elif type_code == 0x05 #-BOOL-# return 2\n elif type_code == 0x00 #-INT8-# return 2\n elif type_code == 0x01 #-INT16-# return 3\n elif type_code == 0x02 #-INT32-# return 5\n elif type_code == 0x03 #-STRING-# return 2 + encoded_bytes[offset + 1]\n elif type_code == 0x04 #-BYTES-# return 3 + encoded_bytes.get(offset + 1, 2)\n end\n return 0\n end\n\n # Helper: Read a value with type prefix and return [value, new_offset]\n def _read_typed_value(encoded_bytes, offset)\n if offset >= size(encoded_bytes) return nil end\n var type_code = encoded_bytes[offset]\n offset += 1 # Skip type byte\n \n if type_code == 0x06 #-NIL-# return nil\n elif type_code == 0x05 #-BOOL-#\n return encoded_bytes[offset] != 0\n elif type_code == 0x00 #-INT8-# \n var v = encoded_bytes[offset]\n return v > 127 ? v - 256 : v\n elif type_code == 0x01 #-INT16-#\n var v = encoded_bytes.get(offset, 2)\n return v > 32767 ? v - 65536 : v\n elif type_code == 0x02 #-INT32-#\n return encoded_bytes.get(offset, 4)\n elif type_code == 0x03 #-STRING-#\n var len = encoded_bytes[offset]\n return encoded_bytes[offset + 1 .. offset + len].asstring()\n elif type_code == 0x04 #-BYTES-#\n var len = encoded_bytes.get(offset, 2)\n return encoded_bytes[offset + 2 .. offset + len + 1]\n end\n return nil\n end\n\n if size(encoded_bytes) < 1 return default end\n var mask = encoded_bytes[0]\n var offset = 1\n \n # Quick check if field exists\n var target_mask = _class._MASK.find(name) # nil or 0..5\n if (target_mask == nil) return default end\n target_mask = (1 << target_mask)\n\n # If no match, quick fail\n if !(mask & target_mask) return default end\n\n # Easy check if 'nillable'\n if target_mask == 0x20 #-IS_NILLABLE-#\n return true # since 'mask & target_mask' is true, we know we should return true\n end\n\n # Skip fields before target\n if target_mask > 0x01 #-HAS_MIN-# && (mask & 0x01 #-HAS_MIN-#)\n offset += _skip_typed_value(encoded_bytes, offset)\n end\n if target_mask > 0x02 #-HAS_MAX-# && (mask & 0x02 #-HAS_MAX-#)\n offset += _skip_typed_value(encoded_bytes, offset)\n end\n if target_mask > 0x04 #-HAS_DEFAULT-# && (mask & 0x04 #-HAS_DEFAULT-#)\n offset += _skip_typed_value(encoded_bytes, offset)\n end\n if target_mask > 0x08 #-HAS_EXPLICIT_TYPE-# && (mask & 0x08 #-HAS_EXPLICIT_TYPE-#)\n offset += 1\n end\n if offset >= size(encoded_bytes) return default end # sanity check\n\n # Special case for explicit_type\n if target_mask == 0x08 #-HAS_EXPLICIT_TYPE-#\n # Read explicit type code and convert to string\n var type_byte = encoded_bytes[offset] # sanity check above guarantees that index is correct\n if type_byte < size(_class._TYPES)\n return _class._TYPES[type_byte]\n end\n return default\n end \n \n # Read target value\n if target_mask == 0x10 #-HAS_ENUM-#\n var count = encoded_bytes[offset]\n offset += 1\n var result = []\n var i = 0\n while i < count\n var val_and_offset = \n result.push(_read_typed_value(encoded_bytes, offset))\n offset += _skip_typed_value(encoded_bytes, offset)\n i += 1\n end\n return result\n end\n\n # All other cases\n return _read_typed_value(encoded_bytes, offset)\n end\nend\n\nreturn {'parameterized_object': parameterized_object}"; modules["core/sequence_manager.be"] = "# Sequence Manager for Animation DSL\n# Handles async execution of animation sequences without blocking delays\n# Supports sub-sequences and repeat logic through recursive composition\n#\n# Extends parameterized_object to provide parameter management and playable interface,\n# allowing sequences to be treated uniformly with animations by the engine.\n#\n# Memory-optimized: Uses two parallel arrays instead of array of maps\n# - step_durations: encodes type and duration in a single integer\n# - step_refs: stores references (animation, closure, sequence_manager, or nil)\n#\n# Duration encoding:\n# >= 0: play (ref=animation) or wait (ref=nil)\n# -2: closure (ref=closure function)\n# -3: subsequence (ref=sequence_manager)\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass sequence_manager : animation.parameterized_object\n # static var DURATION_CLOSURE = -2\n # static var DURATION_SUBSEQUENCE = -3\n \n # Non-parameter instance variables\n var active_sequence # Currently running sequence\n var sequence_state # Current sequence execution state\n var step_index # Current step in the sequence\n var step_start_time # When current step started\n var step_durations # List of duration/type values (int)\n var step_refs # List of references (animation, closure, sequence_manager, or nil)\n \n # Repeat-specific properties\n var repeat_count # Number of times to repeat this sequence (-1 for forever, 0 for no repeat)\n var current_iteration # Current iteration (0-based)\n var is_repeat_sequence # Whether this is a repeat sub-sequence\n \n def init(engine, repeat_count)\n # Initialize parameter system with engine\n super(self).init(engine)\n \n # Initialize non-parameter instance variables\n self.active_sequence = nil\n self.sequence_state = {}\n self.step_index = 0\n self.step_start_time = 0\n self.step_durations = []\n self.step_refs = []\n \n # Repeat logic\n self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once (can be function or number)\n self.current_iteration = 0\n self.is_repeat_sequence = repeat_count != nil && repeat_count != 1\n end\n \n # Add a play step directly\n def push_play_step(animation_ref, duration)\n self.step_durations.push(duration != nil ? duration : 0)\n self.step_refs.push(animation_ref)\n return self\n end\n \n # Add a wait step directly\n def push_wait_step(duration)\n self.step_durations.push(duration)\n self.step_refs.push(nil)\n return self\n end\n \n # Add a closure step directly (used for both assign and log steps)\n def push_closure_step(closure)\n self.step_durations.push(-2 #-self.DURATION_CLOSURE-#)\n self.step_refs.push(closure)\n return self\n end\n \n # Add a repeat subsequence step directly\n def push_repeat_subsequence(sequence_manager)\n self.step_durations.push(-3 #-self.DURATION_SUBSEQUENCE-#)\n self.step_refs.push(sequence_manager)\n return self\n end\n\n # Start this sequence\n def start(time_ms)\n # Stop any current sequence\n if self.is_running\n self.is_running = false\n # Stop any sub-sequences\n self.stop_all_subsequences()\n end\n \n # Initialize sequence state\n self.step_index = 0\n self.step_start_time = time_ms\n self.current_iteration = 0\n self.is_running = true\n \n # Always set start_time for restart behavior\n self.start_time = time_ms\n \n # FIXED: Check repeat count BEFORE starting execution\n # If repeat_count is 0, don't execute at all\n var resolved_repeat_count = self.get_resolved_repeat_count()\n if resolved_repeat_count == 0\n self.is_running = false\n return self\n end\n \n # Push iteration context to engine stack if this is a repeat sequence\n if self.is_repeat_sequence\n self.engine.push_iteration_context(self.current_iteration)\n end\n \n # Start executing if we have steps\n var num_steps = size(self.step_durations)\n if num_steps > 0\n # Execute all consecutive closure steps at the beginning atomically\n while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#\n var closure_func = self.step_refs[self.step_index]\n if closure_func != nil\n closure_func(self.engine)\n end\n self.step_index += 1\n end\n \n # Now execute the next non-closure step (usually play)\n if self.step_index < num_steps\n self.execute_current_step(time_ms)\n end\n end\n \n return self\n end\n \n # Stop this sequence manager\n def stop()\n if self.is_running\n self.is_running = false\n \n # Pop iteration context from engine stack if this is a repeat sequence\n if self.is_repeat_sequence\n self.engine.pop_iteration_context()\n end\n \n # Stop any currently playing animations\n var num_steps = size(self.step_durations)\n if self.step_index < num_steps\n var dur = self.step_durations[self.step_index]\n var ref = self.step_refs[self.step_index]\n if dur == -3 #-self.DURATION_SUBSEQUENCE-#\n ref.stop()\n elif dur != -2 #-self.DURATION_CLOSURE-# && ref != nil\n # play step (dur is >= 0 or function, ref is animation)\n self.engine.remove(ref)\n end\n end\n \n # Stop all sub-sequences (but don't clear entire engine)\n self.stop_all_subsequences()\n end\n return self\n end\n \n # Stop all sub-sequences in our steps\n def stop_all_subsequences()\n var num_steps = size(self.step_durations)\n var i = 0\n while i < num_steps\n if self.step_durations[i] == -3 #-self.DURATION_SUBSEQUENCE-#\n self.step_refs[i].stop()\n end\n i += 1\n end\n return self\n end\n\n # Update sequence state - called from fast_loop\n def update(current_time)\n var num_steps = size(self.step_durations)\n if !self.is_running || num_steps == 0 || self.step_index >= num_steps\n return\n end\n \n var dur = self.step_durations[self.step_index]\n \n # Only closure/subsequence have negative int markers, play/wait have >= 0 or function\n if dur == -3 #-self.DURATION_SUBSEQUENCE-#\n # Handle sub-sequence\n var sub_seq = self.step_refs[self.step_index]\n sub_seq.update(current_time)\n if !sub_seq.is_running\n self.advance_to_next_step(current_time)\n end\n elif dur == -2 #-self.DURATION_CLOSURE-#\n # Closure steps are handled in batches\n self.execute_closure_steps_batch(current_time)\n else\n # Handle regular steps with duration (play or wait)\n # dur is either int >= 0, function, or bool\n var duration_value = dur\n if type(dur) == \"function\"\n duration_value = dur(self.engine)\n end\n duration_value = int(duration_value) # Force int, works for bool\n \n if duration_value > 0\n if current_time - self.step_start_time >= duration_value\n self.advance_to_next_step(current_time)\n end\n else\n # Duration is 0 - complete immediately\n self.advance_to_next_step(current_time)\n end\n end\n end\n \n # Execute the current step\n # skip_budget limits how many consecutive immediate skips to prevent infinite loops\n def execute_current_step(current_time, skip_budget)\n if skip_budget == nil\n skip_budget = size(self.step_durations) + 1 # Default: allow skipping all steps once\n end\n \n var num_steps = size(self.step_durations)\n if self.step_index >= num_steps\n self.complete_iteration(current_time, skip_budget)\n return\n end\n \n var dur = self.step_durations[self.step_index]\n var ref = self.step_refs[self.step_index]\n \n if dur == -3 #-self.DURATION_SUBSEQUENCE-#\n # Start sub-sequence\n ref.start(current_time)\n # If subsequence immediately completed (e.g., repeat_count resolved to 0),\n # advance to next step immediately to avoid black frame (if we have budget)\n if !ref.is_running && skip_budget > 0\n self.advance_to_next_step(current_time, skip_budget - 1)\n return\n end\n elif dur == -2 #-self.DURATION_CLOSURE-#\n # Closure steps should be handled in batches\n if ref != nil\n ref(self.engine)\n end\n elif ref != nil\n # Play step (dur >= 0 and ref is animation)\n # Check if animation is already in the engine\n var animations = self.engine.get_animations()\n var already_added = false\n for existing_anim : animations\n if existing_anim == ref\n already_added = true\n break\n end\n end\n \n if !already_added\n self.engine.add(ref)\n end\n \n # Always restart the animation to ensure proper timing\n ref.start(current_time)\n end\n # else: wait step (dur >= 0 and ref is nil) - nothing to do\n \n self.step_start_time = current_time\n end\n\n # Advance to the next step in the sequence\n def advance_to_next_step(current_time, skip_budget)\n var num_steps = size(self.step_durations)\n\n if skip_budget == nil\n skip_budget = num_steps + 1\n end\n \n # Get current animation ref BEFORE advancing (for atomic transition)\n # If dur is not a special marker and ref is not nil, it's a play step\n var dur = self.step_durations[self.step_index]\n var ref = self.step_refs[self.step_index]\n var current_anim = nil\n if dur != -2 #-self.DURATION_CLOSURE-# && dur != -3 #-self.DURATION_SUBSEQUENCE-# && ref != nil\n current_anim = ref\n end\n \n self.step_index += 1\n \n if self.step_index >= num_steps\n if current_anim != nil\n self.engine.remove(current_anim)\n end\n self.complete_iteration(current_time, skip_budget)\n else\n self.execute_closure_steps_batch_atomic(current_time, current_anim, skip_budget)\n end\n end\n \n # Execute all consecutive closure steps in a batch to avoid black frames\n def execute_closure_steps_batch(current_time)\n var num_steps = size(self.step_durations)\n \n # Execute all consecutive closure steps\n while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#\n var closure_func = self.step_refs[self.step_index]\n if closure_func != nil\n closure_func(self.engine)\n end\n self.step_index += 1\n end\n \n # Now execute the next non-closure step\n if self.step_index < num_steps\n self.execute_current_step(current_time)\n else\n self.complete_iteration(current_time)\n end\n end\n \n # Atomic batch execution to eliminate black frames\n def execute_closure_steps_batch_atomic(current_time, previous_anim, skip_budget)\n if skip_budget == nil\n skip_budget = size(self.step_durations) + 1\n end\n \n var num_steps = size(self.step_durations)\n \n # Execute all consecutive closure steps\n while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#\n var closure_func = self.step_refs[self.step_index]\n if closure_func != nil\n closure_func(self.engine)\n end\n self.step_index += 1\n end\n \n # Check if the next step is the SAME animation\n var is_same_animation = false\n if self.step_index < num_steps && previous_anim != nil\n var next_dur = self.step_durations[self.step_index]\n var next_ref = self.step_refs[self.step_index]\n # If not a special marker and same animation ref\n if next_dur != -2 #-self.DURATION_CLOSURE-# && next_dur != -3 #-self.DURATION_SUBSEQUENCE-# && next_ref == previous_anim\n is_same_animation = true\n end\n end\n \n if is_same_animation\n # Same animation continuing - don't remove/re-add, but DO restart for timing sync\n self.step_start_time = current_time\n previous_anim.start(current_time)\n else\n # Different animation or no next animation\n # Start the next animation BEFORE removing the previous one\n if self.step_index < num_steps\n self.execute_current_step(current_time, skip_budget)\n end\n \n # NOW it's safe to remove the previous animation (no gap)\n if previous_anim != nil\n self.engine.remove(previous_anim)\n end\n end\n \n # Handle completion\n if self.step_index >= num_steps\n self.complete_iteration(current_time, skip_budget)\n end\n end\n\n # Complete current iteration and check if we should repeat\n def complete_iteration(current_time, skip_budget)\n if skip_budget == nil\n skip_budget = size(self.step_durations) + 1\n end\n \n self.current_iteration += 1\n \n # Update iteration context in engine stack if this is a repeat sequence\n if self.is_repeat_sequence\n self.engine.update_current_iteration(self.current_iteration)\n end\n \n # Resolve repeat count (may be a function)\n var resolved_repeat_count = self.get_resolved_repeat_count()\n \n # Check if we should continue repeating\n if resolved_repeat_count == -1 || self.current_iteration < resolved_repeat_count\n # If we've exhausted skip budget, stop here and let next update() continue\n # This prevents infinite loops when all steps are skipped\n if skip_budget <= 0\n self.step_index = 0\n return\n end\n \n # Start next iteration - execute all initial closures atomically\n self.step_index = 0\n var num_steps = size(self.step_durations)\n \n # Execute all consecutive closure steps at the beginning atomically\n while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#\n var closure_func = self.step_refs[self.step_index]\n if closure_func != nil\n closure_func(self.engine)\n end\n self.step_index += 1\n end\n \n # Now execute the next non-closure step (usually play)\n if self.step_index < num_steps\n self.execute_current_step(current_time, skip_budget - 1)\n end\n else\n # All iterations complete\n self.is_running = false\n \n # Pop iteration context from engine stack if this is a repeat sequence\n if self.is_repeat_sequence\n self.engine.pop_iteration_context()\n end\n end\n end\n \n # Resolve repeat count (handle both functions and numbers)\n # Converts booleans to integers: true -> 1, false -> 0\n def get_resolved_repeat_count()\n var count = nil\n if type(self.repeat_count) == \"function\"\n count = self.repeat_count(self.engine)\n else\n count = self.repeat_count\n end\n \n # Convert to integer (handles booleans: true -> 1, false -> 0)\n return int(count)\n end\n \n # Check if sequence is running\n def is_sequence_running()\n return self.is_running\n end\nend\n\nreturn {'sequence_manager': sequence_manager }\n"; modules["core/user_functions.be"] = "# User-Defined Functions Registry for Berry Animation Framework\n# This module manages external Berry functions that can be called from DSL code\n\n# Register a Berry function for DSL use\ndef register_user_function(name, func)\n animation._user_functions[name] = func\nend\n\n# Retrieve a registered function by name\ndef get_user_function(name)\n return animation._user_functions.find(name)\nend\n\n# Check if a function is registered\ndef is_user_function(name)\n return animation._user_functions.contains(name)\nend\n\n# List all registered function names\ndef list_user_functions()\n var names = []\n for name : animation.user_functions.keys()\n names.push(name)\n end\n return names\nend\n\n# Export all functions\nreturn {\n \"register_user_function\": register_user_function,\n \"get_user_function\": get_user_function,\n \"is_user_function\": is_user_function,\n \"list_user_functions\": list_user_functions\n}"; modules["dsl/all_wled_palettes.be"] = "# WLED Palettes converted to Berry format\n# Auto-generated from from_wled/src/wled_palettes.h\n# Total palettes: 59\n#\n# Format: VRGB (Value/Position, Red, Green, Blue) as hex bytes\n\n# Gradient palette \"ib_jul01_gp\", originally from http://seaviewsensing.com/pub/cpt-city/ing/xmas/ib_jul01.c3g\nvar PALETTE_JUL_ = bytes(\n \"00E2060C\" # pos=0 rgb(226,6,12)\n \"5E1A604E\" # pos=94 rgb(26,96,78)\n \"8482BD5E\" # pos=132 rgb(130,189,94)\n \"FFB10309\" # pos=255 rgb(177,3,9)\n)\n\n# Gradient palette \"es_vintage_57_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_57.c3g\nvar PALETTE_GRINTAGE_ = bytes(\n \"001D0803\" # pos=0 rgb(29,8,3)\n \"354C0100\" # pos=53 rgb(76,1,0)\n \"688E601C\" # pos=104 rgb(142,96,28)\n \"99D3BF3D\" # pos=153 rgb(211,191,61)\n \"FF75812A\" # pos=255 rgb(117,129,42)\n)\n\n# Gradient palette \"es_vintage_01_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_01.c3g\nvar PALETTE_VINTAGE_ = bytes(\n \"00291218\" # pos=0 rgb(41,18,24)\n \"33490016\" # pos=51 rgb(73,0,22)\n \"4CA5AA26\" # pos=76 rgb(165,170,38)\n \"65FFBD50\" # pos=101 rgb(255,189,80)\n \"7F8B3828\" # pos=127 rgb(139,56,40)\n \"99490016\" # pos=153 rgb(73,0,22)\n \"E5291218\" # pos=229 rgb(41,18,24)\n \"FF291218\" # pos=255 rgb(41,18,24)\n)\n\n# Gradient palette \"es_rivendell_15_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/rivendell/es_rivendell_15.c3g\nvar PALETTE_RIVENDELL_ = bytes(\n \"0018452C\" # pos=0 rgb(24,69,44)\n \"65496946\" # pos=101 rgb(73,105,70)\n \"A5818C61\" # pos=165 rgb(129,140,97)\n \"F2C8CCA6\" # pos=242 rgb(200,204,166)\n \"FFC8CCA6\" # pos=255 rgb(200,204,166)\n)\n\n# Gradient palette \"rgi_15_gp\", originally from http://seaviewsensing.com/pub/cpt-city/ds/rgi/rgi_15.c3g\nvar PALETTE_RED_BLUE_ = bytes(\n \"00290E63\" # pos=0 rgb(41,14,99)\n \"1F80184A\" # pos=31 rgb(128,24,74)\n \"3FE32232\" # pos=63 rgb(227,34,50)\n \"5F841F4C\" # pos=95 rgb(132,31,76)\n \"7F2F1D66\" # pos=127 rgb(47,29,102)\n \"9F6D2F65\" # pos=159 rgb(109,47,101)\n \"BFB04264\" # pos=191 rgb(176,66,100)\n \"DF813968\" # pos=223 rgb(129,57,104)\n \"FF54306C\" # pos=255 rgb(84,48,108)\n)\n\n# Gradient palette \"retro2_16_gp\", originally from http://seaviewsensing.com/pub/cpt-city/ma/retro2/retro2_16.c3g\nvar PALETTE_YELLOWOUT_ = bytes(\n \"00DEBF08\" # pos=0 rgb(222,191,8)\n \"FF753401\" # pos=255 rgb(117,52,1)\n)\n\n# Gradient palette \"Analogous_1_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/red/Analogous_1.c3g\nvar PALETTE_ANALOGOUS_ = bytes(\n \"002600FF\" # pos=0 rgb(38,0,255)\n \"3F5600FF\" # pos=63 rgb(86,0,255)\n \"7F8B00FF\" # pos=127 rgb(139,0,255)\n \"BFC40075\" # pos=191 rgb(196,0,117)\n \"FFFF0000\" # pos=255 rgb(255,0,0)\n)\n\n# Gradient palette \"es_pinksplash_08_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/pink_splash/es_pinksplash_08.c3g\nvar PALETTE_SPLASH_ = bytes(\n \"00BA3FFF\" # pos=0 rgb(186,63,255)\n \"7FE30955\" # pos=127 rgb(227,9,85)\n \"AFEACDD5\" # pos=175 rgb(234,205,213)\n \"DDCD26B0\" # pos=221 rgb(205,38,176)\n \"FFCD26B0\" # pos=255 rgb(205,38,176)\n)\n\n# Gradient palette \"es_ocean_breeze_036_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/ocean_breeze/es_ocean_breeze_036.c3g\nvar PALETTE_BREEZE_ = bytes(\n \"00103033\" # pos=0 rgb(16,48,51)\n \"591BA6AF\" # pos=89 rgb(27,166,175)\n \"99C5E9FF\" # pos=153 rgb(197,233,255)\n \"FF009198\" # pos=255 rgb(0,145,152)\n)\n\n# Gradient palette \"departure_gp\", originally from http://seaviewsensing.com/pub/cpt-city/mjf/departure.c3g\nvar PALETTE_DEPARTURE_ = bytes(\n \"00352200\" # pos=0 rgb(53,34,0)\n \"2A563300\" # pos=42 rgb(86,51,0)\n \"3F936C31\" # pos=63 rgb(147,108,49)\n \"54D4A66C\" # pos=84 rgb(212,166,108)\n \"6AEBD4B4\" # pos=106 rgb(235,212,180)\n \"74FFFFFF\" # pos=116 rgb(255,255,255)\n \"8ABFFFC1\" # pos=138 rgb(191,255,193)\n \"9454FF58\" # pos=148 rgb(84,255,88)\n \"AA00FF00\" # pos=170 rgb(0,255,0)\n \"BF00C000\" # pos=191 rgb(0,192,0)\n \"D4008000\" # pos=212 rgb(0,128,0)\n \"FF008000\" # pos=255 rgb(0,128,0)\n)\n\n# Gradient palette \"es_landscape_64_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_64.c3g\nvar PALETTE_LANDSCAPE_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"251F5913\" # pos=37 rgb(31,89,19)\n \"4C48B22B\" # pos=76 rgb(72,178,43)\n \"7F96EB05\" # pos=127 rgb(150,235,5)\n \"80BAEA77\" # pos=128 rgb(186,234,119)\n \"82DEE9FC\" # pos=130 rgb(222,233,252)\n \"99C5DBE7\" # pos=153 rgb(197,219,231)\n \"CC84B3FD\" # pos=204 rgb(132,179,253)\n \"FF1C6BE1\" # pos=255 rgb(28,107,225)\n)\n\n# Gradient palette \"es_landscape_33_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_33.c3g\nvar PALETTE_BEACH_ = bytes(\n \"000C2D00\" # pos=0 rgb(12,45,0)\n \"13655602\" # pos=19 rgb(101,86,2)\n \"26CF8004\" # pos=38 rgb(207,128,4)\n \"3FF3C512\" # pos=63 rgb(243,197,18)\n \"426DC492\" # pos=66 rgb(109,196,146)\n \"FF052707\" # pos=255 rgb(5,39,7)\n)\n\n# Gradient palette \"rainbowsherbet_gp\", originally from http://seaviewsensing.com/pub/cpt-city/ma/icecream/rainbowsherbet.c3g\nvar PALETTE_SHERBET_ = bytes(\n \"00FF6629\" # pos=0 rgb(255,102,41)\n \"2BFF8C5A\" # pos=43 rgb(255,140,90)\n \"56FF335A\" # pos=86 rgb(255,51,90)\n \"7FFF99A9\" # pos=127 rgb(255,153,169)\n \"AAFFFFF9\" # pos=170 rgb(255,255,249)\n \"D171FF55\" # pos=209 rgb(113,255,85)\n \"FF9DFF89\" # pos=255 rgb(157,255,137)\n)\n\n# Gradient palette \"gr65_hult_gp\", originally from http://seaviewsensing.com/pub/cpt-city/hult/gr65_hult.c3g\nvar PALETTE_HULT_ = bytes(\n \"00FBD8FC\" # pos=0 rgb(251,216,252)\n \"30FFC0FF\" # pos=48 rgb(255,192,255)\n \"59EF5FF1\" # pos=89 rgb(239,95,241)\n \"A03399D9\" # pos=160 rgb(51,153,217)\n \"D818B8AE\" # pos=216 rgb(24,184,174)\n \"FF18B8AE\" # pos=255 rgb(24,184,174)\n)\n\n# Gradient palette \"gr64_hult_gp\", originally from http://seaviewsensing.com/pub/cpt-city/hult/gr64_hult.c3g\nvar PALETTE_HULT64_ = bytes(\n \"0018B8AE\" # pos=0 rgb(24,184,174)\n \"4208A296\" # pos=66 rgb(8,162,150)\n \"687C8907\" # pos=104 rgb(124,137,7)\n \"82B2BA16\" # pos=130 rgb(178,186,22)\n \"967C8907\" # pos=150 rgb(124,137,7)\n \"C9069C90\" # pos=201 rgb(6,156,144)\n \"EF008075\" # pos=239 rgb(0,128,117)\n \"FF008075\" # pos=255 rgb(0,128,117)\n)\n\n# Gradient palette \"GMT_drywet_gp\", originally from http://seaviewsensing.com/pub/cpt-city/gmt/GMT_drywet.c3g\nvar PALETTE_DRYWET_ = bytes(\n \"00776121\" # pos=0 rgb(119,97,33)\n \"2AEBC758\" # pos=42 rgb(235,199,88)\n \"54A9EE7C\" # pos=84 rgb(169,238,124)\n \"7F25EEE8\" # pos=127 rgb(37,238,232)\n \"AA0778EC\" # pos=170 rgb(7,120,236)\n \"D41B01AF\" # pos=212 rgb(27,1,175)\n \"FF043365\" # pos=255 rgb(4,51,101)\n)\n\n# Gradient palette \"ib15_gp\", originally from http://seaviewsensing.com/pub/cpt-city/ing/general/ib15.c3g\nvar PALETTE_REWHI_ = bytes(\n \"00B1A0C7\" # pos=0 rgb(177,160,199)\n \"48CD9E95\" # pos=72 rgb(205,158,149)\n \"59E99B65\" # pos=89 rgb(233,155,101)\n \"6BFF5F3F\" # pos=107 rgb(255,95,63)\n \"8DC0626D\" # pos=141 rgb(192,98,109)\n \"FF84659F\" # pos=255 rgb(132,101,159)\n)\n\n# Gradient palette \"Tertiary_01_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/vermillion/Tertiary_01.c3g\nvar PALETTE_TERTIARY_ = bytes(\n \"000019FF\" # pos=0 rgb(0,25,255)\n \"3F268C75\" # pos=63 rgb(38,140,117)\n \"7F56FF00\" # pos=127 rgb(86,255,0)\n \"BFA78C13\" # pos=191 rgb(167,140,19)\n \"FFFF1929\" # pos=255 rgb(255,25,41)\n)\n\n# Gradient palette \"lava_gp\", originally from http://seaviewsensing.com/pub/cpt-city/neota/elem/lava.c3g\nvar PALETTE_FIRE_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"2E4D0000\" # pos=46 rgb(77,0,0)\n \"60B10000\" # pos=96 rgb(177,0,0)\n \"6CC42609\" # pos=108 rgb(196,38,9)\n \"77D74C13\" # pos=119 rgb(215,76,19)\n \"92EB731D\" # pos=146 rgb(235,115,29)\n \"AEFF9929\" # pos=174 rgb(255,153,41)\n \"BCFFB229\" # pos=188 rgb(255,178,41)\n \"CAFFCC29\" # pos=202 rgb(255,204,41)\n \"DAFFE629\" # pos=218 rgb(255,230,41)\n \"EAFFFF29\" # pos=234 rgb(255,255,41)\n \"F4FFFF8F\" # pos=244 rgb(255,255,143)\n \"FFFFFFFF\" # pos=255 rgb(255,255,255)\n)\n\n# Gradient palette \"fierce-ice_gp\", originally from http://seaviewsensing.com/pub/cpt-city/neota/elem/fierce-ice.c3g\nvar PALETTE_ICEFIRE_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"3B003375\" # pos=59 rgb(0,51,117)\n \"770066FF\" # pos=119 rgb(0,102,255)\n \"952699FF\" # pos=149 rgb(38,153,255)\n \"B456CCFF\" # pos=180 rgb(86,204,255)\n \"D9A7E6FF\" # pos=217 rgb(167,230,255)\n \"FFFFFFFF\" # pos=255 rgb(255,255,255)\n)\n\n# Gradient palette \"Colorfull_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Colorfull.c3g\nvar PALETTE_CYANE_ = bytes(\n \"003D9B2C\" # pos=0 rgb(61,155,44)\n \"195FAE4D\" # pos=25 rgb(95,174,77)\n \"3C84C171\" # pos=60 rgb(132,193,113)\n \"5D9AA67D\" # pos=93 rgb(154,166,125)\n \"6AAF8A88\" # pos=106 rgb(175,138,136)\n \"6DB77989\" # pos=109 rgb(183,121,137)\n \"71C2688A\" # pos=113 rgb(194,104,138)\n \"74E1B3A5\" # pos=116 rgb(225,179,165)\n \"7CFFFFC0\" # pos=124 rgb(255,255,192)\n \"A8A7DACB\" # pos=168 rgb(167,218,203)\n \"FF54B6D7\" # pos=255 rgb(84,182,215)\n)\n\n# Gradient palette \"Pink_Purple_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Pink_Purple.c3g\nvar PALETTE_LIGHT_PINK_ = bytes(\n \"004F206D\" # pos=0 rgb(79,32,109)\n \"195A2875\" # pos=25 rgb(90,40,117)\n \"3366307C\" # pos=51 rgb(102,48,124)\n \"4C8D87B9\" # pos=76 rgb(141,135,185)\n \"66B4DEF8\" # pos=102 rgb(180,222,248)\n \"6DD0ECFC\" # pos=109 rgb(208,236,252)\n \"72EDFAFF\" # pos=114 rgb(237,250,255)\n \"7ACEC8EF\" # pos=122 rgb(206,200,239)\n \"95B195DE\" # pos=149 rgb(177,149,222)\n \"B7BB82CB\" # pos=183 rgb(187,130,203)\n \"FFC66FB8\" # pos=255 rgb(198,111,184)\n)\n\n# Gradient palette \"Sunset_Real_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Real.c3g\nvar PALETTE_SUNSET_ = bytes(\n \"00B50000\" # pos=0 rgb(181,0,0)\n \"16DA5500\" # pos=22 rgb(218,85,0)\n \"33FFAA00\" # pos=51 rgb(255,170,0)\n \"55D3554D\" # pos=85 rgb(211,85,77)\n \"87A700A9\" # pos=135 rgb(167,0,169)\n \"C64900BC\" # pos=198 rgb(73,0,188)\n \"FF0000CF\" # pos=255 rgb(0,0,207)\n)\n\n# Gradient palette \"Sunset_Yellow_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Yellow.c3g\nvar PALETTE_PASTEL_ = bytes(\n \"003D87B8\" # pos=0 rgb(61,135,184)\n \"2481BCA9\" # pos=36 rgb(129,188,169)\n \"57CBF19B\" # pos=87 rgb(203,241,155)\n \"64E4ED8D\" # pos=100 rgb(228,237,141)\n \"6BFFE87F\" # pos=107 rgb(255,232,127)\n \"73FBCA82\" # pos=115 rgb(251,202,130)\n \"78F8AC85\" # pos=120 rgb(248,172,133)\n \"80FBCA82\" # pos=128 rgb(251,202,130)\n \"B4FFE87F\" # pos=180 rgb(255,232,127)\n \"DFFFF278\" # pos=223 rgb(255,242,120)\n \"FFFFFC71\" # pos=255 rgb(255,252,113)\n)\n\n# Gradient palette \"Beech_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Beech.c3g\nvar PALETTE_BEECH_ = bytes(\n \"00FFFEEC\" # pos=0 rgb(255,254,236)\n \"0CFFFEEC\" # pos=12 rgb(255,254,236)\n \"16FFFEEC\" # pos=22 rgb(255,254,236)\n \"1ADFE0B2\" # pos=26 rgb(223,224,178)\n \"1CC0C37C\" # pos=28 rgb(192,195,124)\n \"1CB0FFE7\" # pos=28 rgb(176,255,231)\n \"327BFBEC\" # pos=50 rgb(123,251,236)\n \"474AF6F1\" # pos=71 rgb(74,246,241)\n \"5D21E1E4\" # pos=93 rgb(33,225,228)\n \"7800CCD7\" # pos=120 rgb(0,204,215)\n \"8504A8B2\" # pos=133 rgb(4,168,178)\n \"880A848F\" # pos=136 rgb(10,132,143)\n \"8833BDD4\" # pos=136 rgb(51,189,212)\n \"D0179FC9\" # pos=208 rgb(23,159,201)\n \"FF0081BE\" # pos=255 rgb(0,129,190)\n)\n\n# Gradient palette \"Another_Sunset_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Another_Sunset.c3g\nvar PALETTE_SUNSET2_ = bytes(\n \"00AF793E\" # pos=0 rgb(175,121,62)\n \"1D80673C\" # pos=29 rgb(128,103,60)\n \"4454543A\" # pos=68 rgb(84,84,58)\n \"44F8B837\" # pos=68 rgb(248,184,55)\n \"61EFCC5D\" # pos=97 rgb(239,204,93)\n \"7CE6E185\" # pos=124 rgb(230,225,133)\n \"B2667D81\" # pos=178 rgb(102,125,129)\n \"FF001A7D\" # pos=255 rgb(0,26,125)\n)\n\n# Gradient palette \"es_autumn_19_gp\", originally from http://seaviewsensing.com/pub/cpt-city/es/autumn/es_autumn_19.c3g\nvar PALETTE_AUTUMN_ = bytes(\n \"005A0E05\" # pos=0 rgb(90,14,5)\n \"338B290D\" # pos=51 rgb(139,41,13)\n \"54B44611\" # pos=84 rgb(180,70,17)\n \"68C0CA7D\" # pos=104 rgb(192,202,125)\n \"70B18903\" # pos=112 rgb(177,137,3)\n \"7ABEC883\" # pos=122 rgb(190,200,131)\n \"7CC0CA7C\" # pos=124 rgb(192,202,124)\n \"87B18903\" # pos=135 rgb(177,137,3)\n \"8EC2CB76\" # pos=142 rgb(194,203,118)\n \"A3B14411\" # pos=163 rgb(177,68,17)\n \"CC80230C\" # pos=204 rgb(128,35,12)\n \"F94A0502\" # pos=249 rgb(74,5,2)\n \"FF4A0502\" # pos=255 rgb(74,5,2)\n)\n\n# Gradient palette \"BlacK_Blue_Magenta_White_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Blue_Magenta_White.c3g\nvar PALETTE_MAGENTA_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"2A000075\" # pos=42 rgb(0,0,117)\n \"540000FF\" # pos=84 rgb(0,0,255)\n \"7F7100FF\" # pos=127 rgb(113,0,255)\n \"AAFF00FF\" # pos=170 rgb(255,0,255)\n \"D4FF80FF\" # pos=212 rgb(255,128,255)\n \"FFFFFFFF\" # pos=255 rgb(255,255,255)\n)\n\n# Gradient palette \"BlacK_Magenta_Red_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Magenta_Red.c3g\nvar PALETTE_MAGRED_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"3F710075\" # pos=63 rgb(113,0,117)\n \"7FFF00FF\" # pos=127 rgb(255,0,255)\n \"BFFF0075\" # pos=191 rgb(255,0,117)\n \"FFFF0000\" # pos=255 rgb(255,0,0)\n)\n\n# Gradient palette \"BlacK_Red_Magenta_Yellow_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Red_Magenta_Yellow.c3g\nvar PALETTE_YELMAG_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"2A710000\" # pos=42 rgb(113,0,0)\n \"54FF0000\" # pos=84 rgb(255,0,0)\n \"7FFF0075\" # pos=127 rgb(255,0,117)\n \"AAFF00FF\" # pos=170 rgb(255,0,255)\n \"D4FF8075\" # pos=212 rgb(255,128,117)\n \"FFFFFF00\" # pos=255 rgb(255,255,0)\n)\n\n# Gradient palette \"Blue_Cyan_Yellow_gp\", originally from http://seaviewsensing.com/pub/cpt-city/nd/basic/Blue_Cyan_Yellow.c3g\nvar PALETTE_YELBLU_ = bytes(\n \"000000FF\" # pos=0 rgb(0,0,255)\n \"3F0080FF\" # pos=63 rgb(0,128,255)\n \"7F00FFFF\" # pos=127 rgb(0,255,255)\n \"BF71FF75\" # pos=191 rgb(113,255,117)\n \"FFFFFF00\" # pos=255 rgb(255,255,0)\n)\n\n# Custom palette by Aircoookie\nvar PALETTE_ORANGE_TEAL_ = bytes(\n \"0000965C\" # pos=0 rgb(0,150,92)\n \"3700965C\" # pos=55 rgb(0,150,92)\n \"C8FF4800\" # pos=200 rgb(255,72,0)\n \"FFFF4800\" # pos=255 rgb(255,72,0)\n)\n\n# Custom palette by Aircoookie\nvar PALETTE_TIAMAT_ = bytes(\n \"0001020E\" # pos=0 rgb(1,2,14)\n \"21020523\" # pos=33 rgb(2,5,35)\n \"640D875C\" # pos=100 rgb(13,135,92)\n \"782BFFC1\" # pos=120 rgb(43,255,193)\n \"8CF707F9\" # pos=140 rgb(247,7,249)\n \"A0C111D0\" # pos=160 rgb(193,17,208)\n \"B427FF9A\" # pos=180 rgb(39,255,154)\n \"C804D5EC\" # pos=200 rgb(4,213,236)\n \"DC27FC87\" # pos=220 rgb(39,252,135)\n \"F0C1D5FD\" # pos=240 rgb(193,213,253)\n \"FFFFF9FF\" # pos=255 rgb(255,249,255)\n)\n\n# Custom palette by Aircoookie\nvar PALETTE_APRIL_NIGHT_ = bytes(\n \"0001052D\" # pos=0 rgb(1,5,45)\n \"0A01052D\" # pos=10 rgb(1,5,45)\n \"1905A9AF\" # pos=25 rgb(5,169,175)\n \"2801052D\" # pos=40 rgb(1,5,45)\n \"3D01052D\" # pos=61 rgb(1,5,45)\n \"4C2DAF1F\" # pos=76 rgb(45,175,31)\n \"5B01052D\" # pos=91 rgb(1,5,45)\n \"7001052D\" # pos=112 rgb(1,5,45)\n \"7FF99605\" # pos=127 rgb(249,150,5)\n \"8F01052D\" # pos=143 rgb(1,5,45)\n \"A201052D\" # pos=162 rgb(1,5,45)\n \"B2FF5C00\" # pos=178 rgb(255,92,0)\n \"C101052D\" # pos=193 rgb(1,5,45)\n \"D601052D\" # pos=214 rgb(1,5,45)\n \"E5DF2D48\" # pos=229 rgb(223,45,72)\n \"F401052D\" # pos=244 rgb(1,5,45)\n \"FF01052D\" # pos=255 rgb(1,5,45)\n)\n\nvar PALETTE_ORANGERY_ = bytes(\n \"00FF5F17\" # pos=0 rgb(255,95,23)\n \"1EFF5200\" # pos=30 rgb(255,82,0)\n \"3CDF0D08\" # pos=60 rgb(223,13,8)\n \"5A902C02\" # pos=90 rgb(144,44,2)\n \"78FF6E11\" # pos=120 rgb(255,110,17)\n \"96FF4500\" # pos=150 rgb(255,69,0)\n \"B49E0D0B\" # pos=180 rgb(158,13,11)\n \"D2F15211\" # pos=210 rgb(241,82,17)\n \"FFD52504\" # pos=255 rgb(213,37,4)\n)\n\n# inspired by Mark Kriegsman https://gist.github.com/kriegsman/756ea6dcae8e30845b5a\nvar PALETTE_C9_ = bytes(\n \"00B80400\" # pos=0 rgb(184,4,0)\n \"3CB80400\" # pos=60 rgb(184,4,0)\n \"41902C02\" # pos=65 rgb(144,44,2)\n \"7D902C02\" # pos=125 rgb(144,44,2)\n \"82046002\" # pos=130 rgb(4,96,2)\n \"BE046002\" # pos=190 rgb(4,96,2)\n \"C3070758\" # pos=195 rgb(7,7,88)\n \"FF070758\" # pos=255 rgb(7,7,88)\n)\n\nvar PALETTE_SAKURA_ = bytes(\n \"00C4130A\" # pos=0 rgb(196,19,10)\n \"41FF452D\" # pos=65 rgb(255,69,45)\n \"82DF2D48\" # pos=130 rgb(223,45,72)\n \"C3FF5267\" # pos=195 rgb(255,82,103)\n \"FFDF0D11\" # pos=255 rgb(223,13,17)\n)\n\nvar PALETTE_AURORA_ = bytes(\n \"0001052D\" # pos=0 rgb(1,5,45)\n \"4000C817\" # pos=64 rgb(0,200,23)\n \"8000FF00\" # pos=128 rgb(0,255,0)\n \"AA00F32D\" # pos=170 rgb(0,243,45)\n \"C8008707\" # pos=200 rgb(0,135,7)\n \"FF01052D\" # pos=255 rgb(1,5,45)\n)\n\nvar PALETTE_ATLANTICA_ = bytes(\n \"00001C70\" # pos=0 rgb(0,28,112)\n \"322060FF\" # pos=50 rgb(32,96,255)\n \"6400F32D\" # pos=100 rgb(0,243,45)\n \"960C5F52\" # pos=150 rgb(12,95,82)\n \"C819BE5F\" # pos=200 rgb(25,190,95)\n \"FF28AA50\" # pos=255 rgb(40,170,80)\n)\n\nvar PALETTE_C9_2_ = bytes(\n \"00067E02\" # pos=0 rgb(6,126,2)\n \"2D067E02\" # pos=45 rgb(6,126,2)\n \"2E041E72\" # pos=46 rgb(4,30,114)\n \"5A041E72\" # pos=90 rgb(4,30,114)\n \"5BFF0500\" # pos=91 rgb(255,5,0)\n \"87FF0500\" # pos=135 rgb(255,5,0)\n \"88C43902\" # pos=136 rgb(196,57,2)\n \"B4C43902\" # pos=180 rgb(196,57,2)\n \"B5895502\" # pos=181 rgb(137,85,2)\n \"FF895502\" # pos=255 rgb(137,85,2)\n)\n\n# C9, but brighter and with a less purple blue\nvar PALETTE_C9_NEW_ = bytes(\n \"00FF0500\" # pos=0 rgb(255,5,0)\n \"3CFF0500\" # pos=60 rgb(255,5,0)\n \"3DC43902\" # pos=61 rgb(196,57,2)\n \"78C43902\" # pos=120 rgb(196,57,2)\n \"79067E02\" # pos=121 rgb(6,126,2)\n \"B4067E02\" # pos=180 rgb(6,126,2)\n \"B5041E72\" # pos=181 rgb(4,30,114)\n \"FF041E72\" # pos=255 rgb(4,30,114)\n)\n\n# Gradient palette \"temperature_gp\", originally from http://seaviewsensing.com/pub/cpt-city/arendal/temperature.c3g\nvar PALETTE_TEMPERATURE_ = bytes(\n \"00145CAB\" # pos=0 rgb(20,92,171)\n \"0E0F6FBA\" # pos=14 rgb(15,111,186)\n \"1C068ED3\" # pos=28 rgb(6,142,211)\n \"2A02A1E3\" # pos=42 rgb(2,161,227)\n \"3810B5EF\" # pos=56 rgb(16,181,239)\n \"4626BCC9\" # pos=70 rgb(38,188,201)\n \"5456CCC8\" # pos=84 rgb(86,204,200)\n \"638BDBB0\" # pos=99 rgb(139,219,176)\n \"71B6E57D\" # pos=113 rgb(182,229,125)\n \"7FC4E63F\" # pos=127 rgb(196,230,63)\n \"8DF1F016\" # pos=141 rgb(241,240,22)\n \"9BFEDE1E\" # pos=155 rgb(254,222,30)\n \"AAFBC704\" # pos=170 rgb(251,199,4)\n \"B8F79D09\" # pos=184 rgb(247,157,9)\n \"C6F3720F\" # pos=198 rgb(243,114,15)\n \"E2D51E1D\" # pos=226 rgb(213,30,29)\n \"F0972623\" # pos=240 rgb(151,38,35)\n \"FF972623\" # pos=255 rgb(151,38,35)\n)\n\n# Gradient palette \"bhw1_01_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_01.c3g\nvar PALETTE_RETRO_CLOWN_ = bytes(\n \"00F2A826\" # pos=0 rgb(242,168,38)\n \"75E24E50\" # pos=117 rgb(226,78,80)\n \"FFA136E1\" # pos=255 rgb(161,54,225)\n)\n\n# Gradient palette \"bhw1_04_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_04.c3g\nvar PALETTE_CANDY_ = bytes(\n \"00F3F217\" # pos=0 rgb(243,242,23)\n \"0FF2A826\" # pos=15 rgb(242,168,38)\n \"8E6F1597\" # pos=142 rgb(111,21,151)\n \"C64A1696\" # pos=198 rgb(74,22,150)\n \"FF000075\" # pos=255 rgb(0,0,117)\n)\n\n# Gradient palette \"bhw1_05_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_05.c3g\nvar PALETTE_TOXY_REAF_ = bytes(\n \"0002EF7E\" # pos=0 rgb(2,239,126)\n \"FF9123D9\" # pos=255 rgb(145,35,217)\n)\n\n# Gradient palette \"bhw1_06_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_06.c3g\nvar PALETTE_FAIRY_REAF_ = bytes(\n \"00DC13BB\" # pos=0 rgb(220,19,187)\n \"A00CE1DB\" # pos=160 rgb(12,225,219)\n \"DBCBF2DF\" # pos=219 rgb(203,242,223)\n \"FFFFFFFF\" # pos=255 rgb(255,255,255)\n)\n\n# Gradient palette \"bhw1_14_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_14.c3g\nvar PALETTE_SEMI_BLUE_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"0C180426\" # pos=12 rgb(24,4,38)\n \"35370854\" # pos=53 rgb(55,8,84)\n \"502B309F\" # pos=80 rgb(43,48,159)\n \"771F59ED\" # pos=119 rgb(31,89,237)\n \"91323BA6\" # pos=145 rgb(50,59,166)\n \"BA471E62\" # pos=186 rgb(71,30,98)\n \"E91F0F2D\" # pos=233 rgb(31,15,45)\n \"FF000000\" # pos=255 rgb(0,0,0)\n)\n\n# Gradient palette \"bhw1_three_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_three.c3g\nvar PALETTE_PINK_CANDY_ = bytes(\n \"00FFFFFF\" # pos=0 rgb(255,255,255)\n \"2D3240FF\" # pos=45 rgb(50,64,255)\n \"70F210BA\" # pos=112 rgb(242,16,186)\n \"8CFFFFFF\" # pos=140 rgb(255,255,255)\n \"9BF210BA\" # pos=155 rgb(242,16,186)\n \"C4740DA6\" # pos=196 rgb(116,13,166)\n \"FFFFFFFF\" # pos=255 rgb(255,255,255)\n)\n\n# Gradient palette \"bhw1_w00t_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_w00t.c3g\nvar PALETTE_RED_REAF_ = bytes(\n \"00244472\" # pos=0 rgb(36,68,114)\n \"6895C3F8\" # pos=104 rgb(149,195,248)\n \"BCFF0000\" # pos=188 rgb(255,0,0)\n \"FF5E0E09\" # pos=255 rgb(94,14,9)\n)\n\n# Gradient palette \"bhw2_23_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_23.c3g\nvar PALETTE_AQUA_FLASH_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"4282F2F5\" # pos=66 rgb(130,242,245)\n \"60FFFF35\" # pos=96 rgb(255,255,53)\n \"7CFFFFFF\" # pos=124 rgb(255,255,255)\n \"99FFFF35\" # pos=153 rgb(255,255,53)\n \"BC82F2F5\" # pos=188 rgb(130,242,245)\n \"FF000000\" # pos=255 rgb(0,0,0)\n)\n\n# Gradient palette \"bhw2_xc_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_xc.c3g\nvar PALETTE_YELBLU_HOT_ = bytes(\n \"002B1E39\" # pos=0 rgb(43,30,57)\n \"3A490077\" # pos=58 rgb(73,0,119)\n \"7A57004A\" # pos=122 rgb(87,0,74)\n \"9EC53916\" # pos=158 rgb(197,57,22)\n \"B7DA751B\" # pos=183 rgb(218,117,27)\n \"DBEFB120\" # pos=219 rgb(239,177,32)\n \"FFF6F71B\" # pos=255 rgb(246,247,27)\n)\n\n# Gradient palette \"bhw2_45_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_45.c3g\nvar PALETTE_LITE_LIGHT_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"09141516\" # pos=9 rgb(20,21,22)\n \"282E2B31\" # pos=40 rgb(46,43,49)\n \"422E2B31\" # pos=66 rgb(46,43,49)\n \"653D1041\" # pos=101 rgb(61,16,65)\n \"FF000000\" # pos=255 rgb(0,0,0)\n)\n\n# Gradient palette \"bhw2_22_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_22.c3g\nvar PALETTE_RED_FLASH_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"63F20C08\" # pos=99 rgb(242,12,8)\n \"82FDE4A3\" # pos=130 rgb(253,228,163)\n \"9BF20C08\" # pos=155 rgb(242,12,8)\n \"FF000000\" # pos=255 rgb(0,0,0)\n)\n\n# Gradient palette \"bhw3_40_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_40.c3g\nvar PALETTE_BLINK_RED_ = bytes(\n \"00040704\" # pos=0 rgb(4,7,4)\n \"2B28193E\" # pos=43 rgb(40,25,62)\n \"4C3D0F24\" # pos=76 rgb(61,15,36)\n \"6DCF2760\" # pos=109 rgb(207,39,96)\n \"7FFF9CB8\" # pos=127 rgb(255,156,184)\n \"A5B949CF\" # pos=165 rgb(185,73,207)\n \"CC6942F0\" # pos=204 rgb(105,66,240)\n \"FF4D1D4E\" # pos=255 rgb(77,29,78)\n)\n\n# Gradient palette \"bhw3_52_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_52.c3g\nvar PALETTE_RED_SHIFT_ = bytes(\n \"0062165D\" # pos=0 rgb(98,22,93)\n \"2D671649\" # pos=45 rgb(103,22,73)\n \"63C02D38\" # pos=99 rgb(192,45,56)\n \"84EBBB3B\" # pos=132 rgb(235,187,59)\n \"AFE4551A\" # pos=175 rgb(228,85,26)\n \"C9E43830\" # pos=201 rgb(228,56,48)\n \"FF020002\" # pos=255 rgb(2,0,2)\n)\n\n# Gradient palette \"bhw4_097_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_097.c3g\nvar PALETTE_RED_TIDE_ = bytes(\n \"00FB2E00\" # pos=0 rgb(251,46,0)\n \"1CFF8B19\" # pos=28 rgb(255,139,25)\n \"2BF69E3F\" # pos=43 rgb(246,158,63)\n \"3AF6D87B\" # pos=58 rgb(246,216,123)\n \"54F35E0A\" # pos=84 rgb(243,94,10)\n \"72B1410B\" # pos=114 rgb(177,65,11)\n \"8CFFF173\" # pos=140 rgb(255,241,115)\n \"A8B1410B\" # pos=168 rgb(177,65,11)\n \"C4FAE99E\" # pos=196 rgb(250,233,158)\n \"D8FF5E06\" # pos=216 rgb(255,94,6)\n \"FF7E0804\" # pos=255 rgb(126,8,4)\n)\n\n# Gradient palette \"bhw4_017_gp\", originally from http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_017.c3g\nvar PALETTE_CANDY2_ = bytes(\n \"006D6666\" # pos=0 rgb(109,102,102)\n \"192A3147\" # pos=25 rgb(42,49,71)\n \"30796054\" # pos=48 rgb(121,96,84)\n \"49F1D61A\" # pos=73 rgb(241,214,26)\n \"59D8682C\" # pos=89 rgb(216,104,44)\n \"822A3147\" # pos=130 rgb(42,49,71)\n \"A3FFB12F\" # pos=163 rgb(255,177,47)\n \"BAF1D61A\" # pos=186 rgb(241,214,26)\n \"D36D6666\" # pos=211 rgb(109,102,102)\n \"FF14130D\" # pos=255 rgb(20,19,13)\n)\n\nvar PALETTE_TRAFFIC_LIGHT_ = bytes(\n \"00000000\" # pos=0 rgb(0,0,0)\n \"5500FF00\" # pos=85 rgb(0,255,0)\n \"AAFFFF00\" # pos=170 rgb(255,255,0)\n \"FFFF0000\" # pos=255 rgb(255,0,0)\n)\n\nvar PALETTE_AURORA_2_ = bytes(\n \"0011B10D\" # pos=0 rgb(17,177,13)\n \"4079F205\" # pos=64 rgb(121,242,5)\n \"8019AD79\" # pos=128 rgb(25,173,121)\n \"C0FA4D7F\" # pos=192 rgb(250,77,127)\n \"FFAB65DD\" # pos=255 rgb(171,101,221)\n)\n\n# Palette map for easy access by name\nclass WLED_Palettes\n static map = {\n \"Jul\": PALETTE_JUL_,\n \"Grintage\": PALETTE_GRINTAGE_,\n \"Vintage\": PALETTE_VINTAGE_,\n \"Rivendell\": PALETTE_RIVENDELL_,\n \"Red & Blue\": PALETTE_RED_BLUE_,\n \"Yellowout\": PALETTE_YELLOWOUT_,\n \"Analogous\": PALETTE_ANALOGOUS_,\n \"Splash\": PALETTE_SPLASH_,\n \"Breeze\": PALETTE_BREEZE_,\n \"Departure\": PALETTE_DEPARTURE_,\n \"Landscape\": PALETTE_LANDSCAPE_,\n \"Beach\": PALETTE_BEACH_,\n \"Sherbet\": PALETTE_SHERBET_,\n \"Hult\": PALETTE_HULT_,\n \"Hult64\": PALETTE_HULT64_,\n \"Drywet\": PALETTE_DRYWET_,\n \"Rewhi\": PALETTE_REWHI_,\n \"Tertiary\": PALETTE_TERTIARY_,\n \"Fire\": PALETTE_FIRE_,\n \"Icefire\": PALETTE_ICEFIRE_,\n \"Cyane\": PALETTE_CYANE_,\n \"Light Pink\": PALETTE_LIGHT_PINK_,\n \"Sunset\": PALETTE_SUNSET_,\n \"Pastel\": PALETTE_PASTEL_,\n \"Beech\": PALETTE_BEECH_,\n \"Sunset2\": PALETTE_SUNSET2_,\n \"Autumn\": PALETTE_AUTUMN_,\n \"Magenta\": PALETTE_MAGENTA_,\n \"Magred\": PALETTE_MAGRED_,\n \"Yelmag\": PALETTE_YELMAG_,\n \"Yelblu\": PALETTE_YELBLU_,\n \"Orange & Teal\": PALETTE_ORANGE_TEAL_,\n \"Tiamat\": PALETTE_TIAMAT_,\n \"April Night\": PALETTE_APRIL_NIGHT_,\n \"Orangery\": PALETTE_ORANGERY_,\n \"C9\": PALETTE_C9_,\n \"Sakura\": PALETTE_SAKURA_,\n \"Aurora\": PALETTE_AURORA_,\n \"Atlantica\": PALETTE_ATLANTICA_,\n \"C9 2\": PALETTE_C9_2_,\n \"C9 New\": PALETTE_C9_NEW_,\n \"Temperature\": PALETTE_TEMPERATURE_,\n \"Retro Clown\": PALETTE_RETRO_CLOWN_,\n \"Candy\": PALETTE_CANDY_,\n \"Toxy Reaf\": PALETTE_TOXY_REAF_,\n \"Fairy Reaf\": PALETTE_FAIRY_REAF_,\n \"Semi Blue\": PALETTE_SEMI_BLUE_,\n \"Pink Candy\": PALETTE_PINK_CANDY_,\n \"Red Reaf\": PALETTE_RED_REAF_,\n \"Aqua Flash\": PALETTE_AQUA_FLASH_,\n \"Yelblu Hot\": PALETTE_YELBLU_HOT_,\n \"Lite Light\": PALETTE_LITE_LIGHT_,\n \"Red Flash\": PALETTE_RED_FLASH_,\n \"Blink Red\": PALETTE_BLINK_RED_,\n \"Red Shift\": PALETTE_RED_SHIFT_,\n \"Red Tide\": PALETTE_RED_TIDE_,\n \"Candy2\": PALETTE_CANDY2_,\n \"Traffic Light\": PALETTE_TRAFFIC_LIGHT_,\n \"Aurora 2\": PALETTE_AURORA_2_\n }\nend\n\nreturn {\"wled_palettes\": WLED_Palettes}\n"; modules["dsl/lexer.be"] = "# Pull-Mode Lexer v2 for Animation DSL\n# Combines pull-mode interface with original lexer.be implementation\n# Reuses most of the code from lexer.be while providing pull-based token access\n\n# Import token functions and Token class\nimport \"dsl/token.be\" as token_module\nvar Token = token_module[\"Token\"]\n\nclass Lexer\n var source # String - DSL source code\n var position # Integer - current character position\n var line # Integer - current line number (1-based)\n var column # Integer - current column number (1-based)\n var token_position # Integer - current token position (for compatibility)\n \n # Initialize pull lexer with source code\n #\n # @param source: string - DSL source code to tokenize\n def init(source)\n self.source = source != nil ? source : \"\"\n self.position = 0\n self.line = 1\n self.column = 1\n self.token_position = 0\n end\n \n # Pull the next token from the stream\n # This is the main pull-mode interface - generates tokens on demand\n #\n # @return Token - Next token, or nil if at end\n def next_token()\n # Skip whitespace and comments until we find a meaningful token or reach end\n while !self.at_end()\n var start_column = self.column\n var ch = self.advance()\n \n if ch == ' ' || ch == '\\t' || ch == '\\r'\n # Skip whitespace (but not newlines - they can be significant)\n continue\n elif ch == '\\n'\n var token = self.create_token(35 #-animation_dsl.Token.NEWLINE-#, \"\\n\", 1)\n self.line += 1\n self.column = 1\n self.token_position += 1\n return token\n elif ch == '#'\n var token = self.scan_comment()\n self.token_position += 1\n return token\n elif ch == '0' && self.peek() == 'x'\n var token = self.scan_hex_color_0x()\n self.token_position += 1\n return token\n elif self.is_alpha(ch) || ch == '_'\n var token = self.scan_identifier_or_keyword()\n self.token_position += 1\n return token\n elif self.is_digit(ch)\n var token = self.scan_number()\n self.token_position += 1\n return token\n elif ch == '\"' || ch == \"'\"\n # Check for triple quotes\n if (ch == '\"' && self.peek() == '\"' && self.peek_char_ahead(1) == '\"') ||\n (ch == \"'\" && self.peek() == \"'\" && self.peek_char_ahead(1) == \"'\")\n var token = self.scan_triple_quoted_string(ch)\n self.token_position += 1\n return token\n else\n var token = self.scan_string(ch)\n self.token_position += 1\n return token\n end\n elif ch == '$'\n var token = self.scan_variable_reference()\n self.token_position += 1\n return token\n else\n var token = self.scan_operator_or_delimiter(ch)\n self.token_position += 1\n return token\n end\n end\n \n # Reached end of source\n return nil\n end\n \n # Peek at the next token without consuming it\n # Uses position saving/restoring to implement peek\n #\n # @return Token - Next token, or nil if at end\n def peek_token()\n # Save current state\n var saved_position = self.position\n var saved_line = self.line\n var saved_column = self.column\n var saved_token_position = self.token_position\n \n # Get next token\n var token = self.next_token()\n if (token != nil)\n # We haven't reached the end of the file \n # Restore state\n self.position = saved_position\n self.line = saved_line\n self.column = saved_column\n self.token_position = saved_token_position\n end\n \n return token\n end\n \n # Peek ahead by n tokens without consuming them\n # Note: This is less efficient than the array-based version but maintains simplicity\n #\n # @param n: int - Number of tokens to look ahead (1-based)\n # @return Token - Token at position + n, or nil if beyond end\n def peek_ahead(n)\n if n <= 0 return nil end\n \n # Save current state\n var saved_position = self.position\n var saved_line = self.line\n var saved_column = self.column\n var saved_token_position = self.token_position\n \n # Advance n tokens\n var token = nil\n for i : 1..n\n token = self.next_token()\n if token == nil break end\n end\n \n # Restore state\n self.position = saved_position\n self.line = saved_line\n self.column = saved_column\n self.token_position = saved_token_position\n \n return token\n end\n \n # Check if we're at the end of the source\n #\n # @return bool - True if no more characters available\n def at_end()\n return self.position >= size(self.source)\n end\n \n # Reset to beginning of source\n def reset()\n self.position = 0\n self.line = 1\n self.column = 1\n self.token_position = 0\n end\n \n \n # Get current position in token stream (for compatibility with array-based version)\n #\n # @return int - Current token position\n def get_position()\n return self.token_position\n end\n \n # Set position in token stream (for compatibility with array-based version)\n # Note: This is a simplified implementation that resets to beginning and advances\n #\n # @param pos: int - New token position\n def set_position(pos)\n if pos < 0 return end\n \n # Save current state in case we need to restore it\n var saved_position = self.position\n var saved_line = self.line\n var saved_column = self.column\n var saved_token_position = self.token_position\n \n # Reset to beginning\n self.position = 0\n self.line = 1\n self.column = 1\n self.token_position = 0\n \n # Advance to desired token position\n while self.token_position < pos && !self.at_end()\n self.next_token()\n end\n \n # If we didn't reach the desired position, it was invalid - restore state\n if self.token_position != pos\n self.position = saved_position\n self.line = saved_line\n self.column = saved_column\n self.token_position = saved_token_position\n end\n end\n \n # Create a sub-lexer (for compatibility with array-based version)\n # Note: This converts token positions to character positions\n #\n # @param start_token_pos: int - Starting token position\n # @param end_token_pos: int - Ending token position (exclusive)\n # @return Lexer - New pull lexer with subset of source\n def create_sub_lexer(start_token_pos, end_token_pos)\n import animation_dsl\n # Check for invalid ranges\n if start_token_pos < 0 || end_token_pos <= start_token_pos\n # Invalid range - return empty sub-lexer\n return animation_dsl.create_lexer(\"\")\n end\n \n # Save current state\n var saved_position = self.position\n var saved_line = self.line\n var saved_column = self.column\n var saved_token_position = self.token_position\n \n # Reset to beginning and find character positions for token positions\n self.position = 0\n self.line = 1\n self.column = 1\n self.token_position = 0\n \n var start_char_pos = 0\n var end_char_pos = size(self.source)\n var found_start = false\n var found_end = false\n \n # Find start position\n while self.token_position < start_token_pos && !self.at_end()\n start_char_pos = self.position\n self.next_token()\n end\n if self.token_position == start_token_pos\n start_char_pos = self.position\n found_start = true\n end\n \n # Find end position\n while self.token_position < end_token_pos && !self.at_end()\n self.next_token()\n end\n if self.token_position == end_token_pos\n end_char_pos = self.position\n found_end = true\n end\n \n # Restore state\n self.position = saved_position\n self.line = saved_line\n self.column = saved_column\n self.token_position = saved_token_position\n \n # Create sub-lexer with character range\n if !found_start\n return animation_dsl.create_lexer(\"\")\n end\n \n # Clamp end position\n if end_char_pos > size(self.source) end_char_pos = size(self.source) end\n if start_char_pos >= end_char_pos\n return animation_dsl.create_lexer(\"\")\n end\n \n # Extract subset of source\n var sub_source = self.source[start_char_pos..end_char_pos-1]\n var sub_lexer = animation_dsl.create_lexer(sub_source)\n # Ensure sub-lexer starts at position 0 (should already be 0 from init, but make sure)\n sub_lexer.position = 0\n sub_lexer.line = 1\n sub_lexer.column = 1\n sub_lexer.token_position = 0\n return sub_lexer\n end\n \n # === TOKEN SCANNING METHODS (from original lexer.be) ===\n \n # Scan comment (now unambiguous - only starts with #)\n def scan_comment()\n var start_pos = self.position - 1\n var start_column = self.column - 1\n \n # This is a comment - consume until end of line\n while !self.at_end() && self.peek() != '\\n'\n self.advance()\n end\n \n var comment_text = self.source[start_pos..self.position-1]\n \n # Trim trailing whitespace from comment text manually\n # Find the last non-whitespace character in the comment content\n var trimmed_text = comment_text\n var end_pos = size(comment_text) - 1\n while end_pos >= 0 && (comment_text[end_pos] == ' ' || comment_text[end_pos] == '\\t' || comment_text[end_pos] == '\\r')\n end_pos -= 1\n end\n \n # Extract trimmed comment text\n if end_pos >= 0\n trimmed_text = comment_text[0 .. end_pos]\n else\n trimmed_text = \"#\" # Keep at least the # character for empty comments\n end\n \n # Use trimmed text but keep original position tracking\n return self.create_token(37 #-animation_dsl.Token.COMMENT-#, trimmed_text, self.position - start_pos)\n end\n \n # Scan hex color (0xRRGGBB, 0xAARRGGBB)\n def scan_hex_color_0x()\n var start_pos = self.position - 1 # Include the '0'\n var start_column = self.column - 1\n \n # Advance past 'x'\n self.advance()\n var hex_digits = 0\n \n # Count hex digits\n while !self.at_end() && self.is_hex_digit(self.peek())\n self.advance()\n hex_digits += 1\n end\n \n var color_value = self.source[start_pos..self.position-1]\n \n # Validate hex color format - support 6 (RGB) or 8 (ARGB) digits\n if hex_digits == 6 || hex_digits == 8\n return self.create_token(4 #-animation_dsl.Token.COLOR-#, color_value, size(color_value))\n else\n self.error(\"Invalid hex color format: \" + color_value + \" (expected 0xRRGGBB or 0xAARRGGBB)\")\n end\n end\n \n # Scan identifier or keyword\n def scan_identifier_or_keyword()\n import animation_dsl\n var start_pos = self.position - 1\n var start_column = self.column - 1\n \n # Continue while alphanumeric or underscore\n while !self.at_end() && (self.is_alnum(self.peek()) || self.peek() == '_')\n self.advance()\n end\n \n var text = self.source[start_pos..self.position-1]\n var token_type\n \n # Check for color names first (they take precedence over keywords)\n if animation_dsl.is_color_name(text)\n token_type = 4 #-animation_dsl.Token.COLOR-#\n elif animation_dsl.is_keyword(text)\n token_type = 0 #-animation_dsl.Token.KEYWORD-#\n else\n token_type = 1 #-animation_dsl.Token.IDENTIFIER-#\n end\n \n return self.create_token(token_type, text, size(text))\n end\n \n # Scan numeric literal (with optional time/percentage/multiplier suffix)\n def scan_number()\n var start_pos = self.position - 1\n var start_column = self.column - 1\n var has_dot = false\n \n # Scan integer part\n while !self.at_end() && self.is_digit(self.peek())\n self.advance()\n end\n \n # Check for decimal point\n if !self.at_end() && self.peek() == '.' && \n self.position + 1 < size(self.source) && self.is_digit(self.source[self.position + 1])\n has_dot = true\n self.advance() # consume '.'\n \n # Scan fractional part\n while !self.at_end() && self.is_digit(self.peek())\n self.advance()\n end\n end\n \n var number_text = self.source[start_pos..self.position-1]\n \n # Check for time unit suffixes\n if self.check_time_suffix()\n var suffix = self.scan_time_suffix()\n return self.create_token(5 #-animation_dsl.Token.TIME-#, number_text + suffix, size(number_text + suffix))\n # Check for percentage suffix\n elif !self.at_end() && self.peek() == '%'\n self.advance()\n return self.create_token(6 #-animation_dsl.Token.PERCENTAGE-#, number_text + \"%\", size(number_text) + 1)\n # Check for multiplier suffix\n elif !self.at_end() && self.peek() == 'x'\n self.advance()\n return self.create_token(7 #-animation_dsl.Token.MULTIPLIER-#, number_text + \"x\", size(number_text) + 1)\n else\n # Plain number\n return self.create_token(2 #-animation_dsl.Token.NUMBER-#, number_text, size(number_text))\n end\n end\n \n # Check if current position has a time suffix\n def check_time_suffix()\n import string\n if self.at_end()\n return false\n end\n \n var remaining = self.source[self.position..]\n return string.startswith(remaining, \"ms\") ||\n string.startswith(remaining, \"s\") ||\n string.startswith(remaining, \"m\") ||\n string.startswith(remaining, \"h\")\n end\n \n # Scan time suffix and return it\n def scan_time_suffix()\n import string\n if string.startswith(self.source[self.position..], \"ms\")\n self.advance()\n self.advance()\n return \"ms\"\n elif self.peek() == 's'\n self.advance()\n return \"s\"\n elif self.peek() == 'm'\n self.advance()\n return \"m\"\n elif self.peek() == 'h'\n self.advance()\n return \"h\"\n end\n return \"\"\n end\n \n # Scan string literal\n def scan_string(quote_char)\n var start_pos = self.position - 1 # Include opening quote\n var start_column = self.column - 1\n var value = \"\"\n \n while !self.at_end() && self.peek() != quote_char\n var ch = self.advance()\n \n if ch == '\\\\'\n # Handle escape sequences\n if !self.at_end()\n var escaped = self.advance()\n if escaped == 'n'\n value += '\\n'\n elif escaped == 't'\n value += '\\t'\n elif escaped == 'r'\n value += '\\r'\n elif escaped == '\\\\'\n value += '\\\\'\n elif escaped == quote_char\n value += quote_char\n else\n # Unknown escape sequence - include as-is\n value += '\\\\'\n value += escaped\n end\n else\n value += '\\\\'\n end\n elif ch == '\\n'\n self.line += 1\n self.column = 1\n value += ch\n else\n value += ch\n end\n end\n \n if self.at_end()\n self.error(\"Unterminated string literal\")\n else\n # Consume closing quote\n self.advance()\n return self.create_token(3 #-animation_dsl.Token.STRING-#, value, self.position - start_pos)\n end\n end\n \n # Scan triple-quoted string literal (for berry code blocks)\n def scan_triple_quoted_string(quote_char)\n var start_pos = self.position - 1 # Include first opening quote\n var start_column = self.column - 1\n var value = \"\"\n \n # Consume the two remaining opening quotes\n self.advance() # second quote\n self.advance() # third quote\n \n # Look for the closing triple quotes\n while !self.at_end()\n var ch = self.peek()\n \n # Check for closing triple quotes\n if ch == quote_char && \n self.peek_char_ahead(1) == quote_char && \n self.peek_char_ahead(2) == quote_char\n # Found closing triple quotes - consume them\n self.advance() # first closing quote\n self.advance() # second closing quote\n self.advance() # third closing quote\n break\n end\n \n # Regular character - add to value\n ch = self.advance()\n if ch == '\\n'\n self.line += 1\n self.column = 1\n end\n value += ch\n end\n \n # Check if we reached end without finding closing quotes\n if self.at_end() && !(self.source[self.position-3..self.position-1] == quote_char + quote_char + quote_char)\n self.error(\"Unterminated triple-quoted string literal\")\n else\n return self.create_token(3 #-animation_dsl.Token.STRING-#, value, self.position - start_pos)\n end\n end\n \n # Scan variable reference ($identifier)\n def scan_variable_reference()\n var start_pos = self.position - 1 # Include $\n var start_column = self.column - 1\n \n if self.at_end() || !(self.is_alpha(self.peek()) || self.peek() == '_')\n self.error(\"Invalid variable reference: $ must be followed by identifier\")\n end\n \n # Scan identifier part\n while !self.at_end() && (self.is_alnum(self.peek()) || self.peek() == '_')\n self.advance()\n end\n \n var var_ref = self.source[start_pos..self.position-1]\n return self.create_token(36 #-animation_dsl.Token.VARIABLE_REF-#, var_ref, size(var_ref))\n end\n \n # Scan operator or delimiter\n def scan_operator_or_delimiter(ch)\n var start_column = self.column - 1\n \n if ch == '='\n if self.match('=')\n return self.create_token(15 #-animation_dsl.Token.EQUAL-#, \"==\", 2)\n else\n return self.create_token(8 #-animation_dsl.Token.ASSIGN-#, \"=\", 1)\n end\n elif ch == '!'\n if self.match('=')\n return self.create_token(16 #-animation_dsl.Token.NOT_EQUAL-#, \"!=\", 2)\n else\n return self.create_token(23 #-animation_dsl.Token.LOGICAL_NOT-#, \"!\", 1)\n end\n elif ch == '<'\n if self.match('=')\n return self.create_token(18 #-animation_dsl.Token.LESS_EQUAL-#, \"<=\", 2)\n elif self.match('<')\n # Left shift - not used in DSL but included for completeness\n self.error(\"Left shift operator '<<' not supported in DSL\")\n else\n return self.create_token(17 #-animation_dsl.Token.LESS_THAN-#, \"<\", 1)\n end\n elif ch == '>'\n if self.match('=')\n return self.create_token(20 #-animation_dsl.Token.GREATER_EQUAL-#, \">=\", 2)\n elif self.match('>')\n # Right shift - not used in DSL but included for completeness\n self.error(\"Right shift operator '>>' not supported in DSL\")\n else\n return self.create_token(19 #-animation_dsl.Token.GREATER_THAN-#, \">\", 1)\n end\n elif ch == '&'\n if self.match('&')\n return self.create_token(21 #-animation_dsl.Token.LOGICAL_AND-#, \"&&\", 2)\n else\n self.error(\"Single '&' not supported in DSL\")\n end\n elif ch == '|'\n if self.match('|')\n return self.create_token(22 #-animation_dsl.Token.LOGICAL_OR-#, \"||\", 2)\n else\n self.error(\"Single '|' not supported in DSL\")\n end\n elif ch == '-'\n if self.match('>')\n return self.create_token(34 #-animation_dsl.Token.ARROW-#, \"->\", 2)\n else\n return self.create_token(10 #-animation_dsl.Token.MINUS-#, \"-\", 1)\n end\n elif ch == '+'\n return self.create_token(9 #-animation_dsl.Token.PLUS-#, \"+\", 1)\n elif ch == '*'\n return self.create_token(11 #-animation_dsl.Token.MULTIPLY-#, \"*\", 1)\n elif ch == '/'\n return self.create_token(12 #-animation_dsl.Token.DIVIDE-#, \"/\", 1)\n elif ch == '%'\n return self.create_token(13 #-animation_dsl.Token.MODULO-#, \"%\", 1)\n elif ch == '^'\n return self.create_token(14 #-animation_dsl.Token.POWER-#, \"^\", 1)\n elif ch == '('\n return self.create_token(24 #-animation_dsl.Token.LEFT_PAREN-#, \"(\", 1)\n elif ch == ')'\n return self.create_token(25 #-animation_dsl.Token.RIGHT_PAREN-#, \")\", 1)\n elif ch == '{'\n return self.create_token(26 #-animation_dsl.Token.LEFT_BRACE-#, \"{\", 1)\n elif ch == '}'\n return self.create_token(27 #-animation_dsl.Token.RIGHT_BRACE-#, \"}\", 1)\n elif ch == '['\n return self.create_token(28 #-animation_dsl.Token.LEFT_BRACKET-#, \"[\", 1)\n elif ch == ']'\n return self.create_token(29 #-animation_dsl.Token.RIGHT_BRACKET-#, \"]\", 1)\n elif ch == ','\n return self.create_token(30 #-animation_dsl.Token.COMMA-#, \",\", 1)\n elif ch == ';'\n return self.create_token(31 #-animation_dsl.Token.SEMICOLON-#, \";\", 1)\n elif ch == ':'\n return self.create_token(32 #-animation_dsl.Token.COLON-#, \":\", 1)\n elif ch == '.'\n # For now, just handle single dots - range operators can be added later if needed\n return self.create_token(33 #-animation_dsl.Token.DOT-#, \".\", 1)\n else\n self.error(\"Unexpected character: '\" + ch + \"'\")\n end\n end\n \n # === HELPER METHODS (from original lexer.be) ===\n \n # Advance position and return current character\n def advance()\n if self.at_end()\n return \"\"\n end\n \n var ch = self.source[self.position]\n self.position += 1\n self.column += 1\n return ch\n end\n \n # Peek at current character without advancing\n def peek()\n if self.at_end()\n return \"\"\n end\n return self.source[self.position]\n end\n \n # Peek ahead by n characters without advancing\n def peek_char_ahead(n)\n if self.position + n >= size(self.source)\n return \"\"\n end\n return self.source[self.position + n]\n end\n \n # Check if current character matches expected and advance if so\n def match(expected)\n if self.at_end() || self.source[self.position] != expected\n return false\n end\n \n self.position += 1\n self.column += 1\n return true\n end\n \n # Character classification helpers\n def is_alpha(ch)\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n end\n \n def is_digit(ch)\n return ch >= '0' && ch <= '9'\n end\n \n def is_alnum(ch)\n return self.is_alpha(ch) || self.is_digit(ch)\n end\n \n def is_hex_digit(ch)\n return self.is_digit(ch) || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')\n end\n \n # Create token with proper position tracking\n def create_token(token_type, value, length)\n import animation_dsl\n return animation_dsl.Token(token_type, value, self.line, self.column - length, length)\n end\n \n # Raise lexical error immediately\n def error(message)\n var error_msg = \"Line \" + str(self.line) + \":\" + str(self.column) + \": \" + message\n raise \"lexical_error\", error_msg\n end\nend\n\nreturn {\n \"create_lexer\": Lexer\n}"; modules["dsl/named_colors.be"] = "# Named Colors Module for Animation DSL\n# Provides color name to ARGB value mappings for the DSL transpiler\n\n# Static color mapping for named colors (helps with solidification)\n# Maps color names to ARGB integer values (0xAARRGGBB format)\n# All colors have full alpha (0xFF) except transparent\nvar named_colors = {\n # Primary colors\n \"red\": 0xFFFF0000, # Pure red\n \"green\": 0xFF008000, # HTML/CSS standard green (darker, more readable)\n \"blue\": 0xFF0000FF, # Pure blue\n \n # Achromatic colors\n \"white\": 0xFFFFFFFF, # Pure white\n \"black\": 0xFF000000, # Pure black\n \"gray\": 0xFF808080, # Medium gray\n \"grey\": 0xFF808080, # Alternative spelling\n \"silver\": 0xFFC0C0C0, # Light gray\n \n # Secondary colors\n \"yellow\": 0xFFFFFF00, # Pure yellow (red + green)\n \"cyan\": 0xFF00FFFF, # Pure cyan (green + blue)\n \"magenta\": 0xFFFF00FF, # Pure magenta (red + blue)\n \n # Extended web colors\n \"orange\": 0xFFFFA500, # Orange\n \"purple\": 0xFF800080, # Purple (darker magenta)\n \"pink\": 0xFFFFC0CB, # Light pink\n \"lime\": 0xFF00FF00, # Pure green (HTML/CSS lime = full intensity)\n \"navy\": 0xFF000080, # Dark blue\n \"olive\": 0xFF808000, # Dark yellow-green\n \"maroon\": 0xFF800000, # Dark red\n \"teal\": 0xFF008080, # Dark cyan\n \"aqua\": 0xFF00FFFF, # Same as cyan\n \"fuchsia\": 0xFFFF00FF, # Same as magenta\n \n # Precious metals\n \"gold\": 0xFFFFD700, # Metallic gold\n \n # Natural colors\n \"brown\": 0xFFA52A2A, # Saddle brown\n \"tan\": 0xFFD2B48C, # Light brown/beige\n \"beige\": 0xFFF5F5DC, # Very light brown\n \"ivory\": 0xFFFFFFF0, # Off-white with yellow tint\n \"snow\": 0xFFFFFAFA, # Off-white with slight blue tint\n \n # Flower/nature colors\n \"indigo\": 0xFF4B0082, # Deep blue-purple\n \"violet\": 0xFFEE82EE, # Light purple\n \"crimson\": 0xFFDC143C, # Deep red\n \"coral\": 0xFFFF7F50, # Orange-pink\n \"salmon\": 0xFFFA8072, # Pink-orange\n \"khaki\": 0xFFF0E68C, # Pale yellow-brown\n \"plum\": 0xFFDDA0DD, # Light purple\n \"orchid\": 0xFFDA70D6, # Medium purple\n \"turquoise\": 0xFF40E0D0, # Blue-green\n \n # Special\n \"transparent\": 0x00000000 # Fully transparent (alpha = 0)\n}\n\nreturn {\"named_colors\": named_colors}\n"; modules["dsl/symbol_table.be"] = "# Symbol Table Classes for DSL Transpiler\n# Enhanced symbol caching and management for the Animation DSL\n\n# Symbol table entry class for enhanced symbol caching\nclass SymbolEntry\n # Type constants\n static var TYPE_PALETTE_CONSTANT = 1\n static var TYPE_PALETTE = 2\n static var TYPE_CONSTANT = 3\n static var TYPE_MATH_FUNCTION = 4\n static var TYPE_USER_FUNCTION = 5\n static var TYPE_VALUE_PROVIDER_CONSTRUCTOR = 6\n static var TYPE_VALUE_PROVIDER = 7\n static var TYPE_ANIMATION_CONSTRUCTOR = 8\n static var TYPE_ANIMATION = 9\n static var TYPE_COLOR_CONSTRUCTOR = 10\n static var TYPE_COLOR = 11\n static var TYPE_VARIABLE = 12\n static var TYPE_SEQUENCE = 13\n static var TYPE_TEMPLATE = 14\n \n var name # Symbol name\n var type # Symbol type (int constant)\n var instance # Actual instance (for validation) or nil\n var takes_args # Boolean: whether this symbol takes arguments\n var arg_type # \"positional\", \"named\", or \"none\"\n var is_builtin # Boolean: whether this is a built-in symbol from animation module\n var is_dangerous # Boolean: whether calling this symbol creates a new instance (dangerous in computed expressions)\n var param_types # Map of parameter names to types (for templates and user functions)\n \n def init(name, typ, instance, is_builtin)\n self.name = name\n self.type = typ\n self.instance = instance\n self.is_builtin = is_builtin != nil ? is_builtin : false\n self.takes_args = false\n self.arg_type = \"none\"\n self.is_dangerous = false\n self.param_types = {}\n \n # Auto-detect argument characteristics and danger level based on type\n self._detect_arg_characteristics()\n self._detect_danger_level()\n end\n \n # Detect if this symbol takes arguments and what type\n def _detect_arg_characteristics()\n if self.type == self.TYPE_PALETTE_CONSTANT || self.type == self.TYPE_PALETTE || self.type == self.TYPE_CONSTANT\n # Palette objects and constants don't take arguments\n self.takes_args = false\n self.arg_type = \"none\"\n elif self.type == self.TYPE_MATH_FUNCTION\n # Math functions like max, min take positional arguments\n self.takes_args = true\n self.arg_type = \"positional\"\n elif self.type == self.TYPE_USER_FUNCTION\n # User functions take positional arguments (engine + user args)\n self.takes_args = true\n self.arg_type = \"positional\"\n elif self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR || self.type == self.TYPE_ANIMATION_CONSTRUCTOR || self.type == self.TYPE_COLOR_CONSTRUCTOR\n # Constructor functions take named arguments\n self.takes_args = true\n self.arg_type = \"named\"\n else\n # Instances, variables, sequences, templates don't take arguments when referenced\n self.takes_args = false\n self.arg_type = \"none\"\n end\n end\n \n # Detect if this symbol is dangerous (creates new instances when called)\n def _detect_danger_level()\n if self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR\n # Value provider constructors create new instances - dangerous in computed expressions\n self.is_dangerous = true\n elif self.type == self.TYPE_ANIMATION_CONSTRUCTOR\n # Animation constructors create new instances - dangerous in computed expressions\n self.is_dangerous = true\n elif self.type == self.TYPE_COLOR_CONSTRUCTOR\n # Color provider constructors create new instances - dangerous in computed expressions\n self.is_dangerous = true\n else\n # Constants, math functions, variables, instances, user functions, etc. are safe\n self.is_dangerous = false\n end\n end\n \n # Check if this symbol is a bytes() instance (for palettes)\n def is_bytes_instance()\n return (self.type == self.TYPE_PALETTE_CONSTANT || self.type == self.TYPE_PALETTE) && self.instance != nil && isinstance(self.instance, bytes)\n end\n \n # Check if this symbol is a math function\n def is_math_function()\n return self.type == self.TYPE_MATH_FUNCTION\n end\n \n # Check if this symbol is a user function\n def is_user_function()\n return self.type == self.TYPE_USER_FUNCTION\n end\n \n \n # Check if this symbol is a value provider constructor\n def is_value_provider_constructor()\n return self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR\n end\n \n # Check if this symbol is a value provider instance\n def is_value_provider_instance()\n return self.type == self.TYPE_VALUE_PROVIDER\n end\n \n # Check if this symbol is an animation constructor\n def is_animation_constructor()\n return self.type == self.TYPE_ANIMATION_CONSTRUCTOR\n end\n \n # Check if this symbol is an animation instance\n def is_animation_instance()\n return self.type == self.TYPE_ANIMATION\n end\n \n # Check if this symbol is a color constructor\n def is_color_constructor()\n return self.type == self.TYPE_COLOR_CONSTRUCTOR\n end\n \n # Check if this symbol is a color instance\n def is_color_instance()\n return self.type == self.TYPE_COLOR\n end\n \n # Check if this symbol takes positional arguments\n def takes_positional_args()\n return self.takes_args && self.arg_type == \"positional\"\n end\n \n # Check if this symbol takes named arguments\n def takes_named_args()\n return self.takes_args && self.arg_type == \"named\"\n end\n \n # Check if this symbol is dangerous (creates new instances when called)\n def is_dangerous_call()\n return self.is_dangerous\n end\n \n # Set parameter types for templates and user functions\n def set_param_types(param_types)\n self.param_types = param_types != nil ? param_types : {}\n end\n \n # Get parameter types\n def get_param_types()\n return self.param_types\n end\n \n # Convert type constant to string for debugging\n def type_to_string()\n if self.type == self.TYPE_PALETTE_CONSTANT return \"palette_constant\"\n elif self.type == self.TYPE_PALETTE return \"palette\"\n elif self.type == self.TYPE_CONSTANT return \"constant\"\n elif self.type == self.TYPE_MATH_FUNCTION return \"math_function\"\n elif self.type == self.TYPE_USER_FUNCTION return \"user_function\"\n elif self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR return \"value_provider_constructor\"\n elif self.type == self.TYPE_VALUE_PROVIDER return \"value_provider\"\n elif self.type == self.TYPE_ANIMATION_CONSTRUCTOR return \"animation_constructor\"\n elif self.type == self.TYPE_ANIMATION return \"animation\"\n elif self.type == self.TYPE_COLOR_CONSTRUCTOR return \"color_constructor\"\n elif self.type == self.TYPE_COLOR return \"color\"\n elif self.type == self.TYPE_VARIABLE return \"variable\"\n elif self.type == self.TYPE_SEQUENCE return \"sequence\"\n elif self.type == self.TYPE_TEMPLATE return \"template\"\n else return f\"unknown({self.type})\"\n end\n end\n \n # Get the resolved symbol reference for code generation\n def get_reference()\n # Generate appropriate reference based on whether it's built-in\n if self.is_builtin\n # Special handling for math functions\n if self.type == self.TYPE_MATH_FUNCTION\n return f\"animation._math.{self.name}\"\n else\n return f\"animation.{self.name}\"\n end\n else\n # User-defined symbols get underscore suffix\n return f\"{self.name}_\"\n end\n end\n \n # String representation for debugging\n def tostring()\n import string\n \n var instance_str = \"nil\"\n if self.instance != nil\n var instance_type = type(self.instance)\n if instance_type == \"instance\"\n instance_str = f\"<{classname(self.instance)}>\"\n else\n instance_str = f\"<{instance_type}:{str(self.instance)}>\"\n end\n end\n \n var param_types_str = \"\"\n if size(self.param_types) > 0\n var params_list = \"\"\n var first = true\n for key : self.param_types.keys()\n if !first\n params_list += \",\"\n end\n params_list += f\"{key}:{self.param_types[key]}\"\n first = false\n end\n param_types_str = f\" params=[{params_list}]\"\n end\n \n return f\"SymbolEntry(name='{self.name}', type='{self.type_to_string()}', instance={instance_str}, \" +\n f\"takes_args={self.takes_args}, arg_type='{self.arg_type}', \" +\n f\"is_builtin={self.is_builtin}, is_dangerous={self.is_dangerous}{param_types_str})\"\n end\n \n # Create a symbol entry for a palette constant (built-in like PALETTE_RAINBOW)\n static def create_palette_constant(name, instance, is_builtin)\n return _class(name, _class.TYPE_PALETTE_CONSTANT, instance, is_builtin)\n end\n \n # Create a symbol entry for a palette instance (user-defined)\n static def create_palette_instance(name, instance, is_builtin)\n return _class(name, _class.TYPE_PALETTE, instance, is_builtin)\n end\n \n # Create a symbol entry for an integer constant\n static def create_constant(name, instance, is_builtin)\n return _class(name, _class.TYPE_CONSTANT, instance, is_builtin)\n end\n \n # Create a symbol entry for a math function\n static def create_math_function(name, is_builtin)\n return _class(name, _class.TYPE_MATH_FUNCTION, nil, is_builtin)\n end\n \n # Create a symbol entry for a user function\n static def create_user_function(name, is_builtin)\n return _class(name, _class.TYPE_USER_FUNCTION, nil, is_builtin)\n end\n \n \n # Create a symbol entry for a value provider constructor (built-in like triangle, smooth)\n static def create_value_provider_constructor(name, instance, is_builtin)\n return _class(name, _class.TYPE_VALUE_PROVIDER_CONSTRUCTOR, instance, is_builtin)\n end\n \n # Create a symbol entry for a value provider instance (user-defined)\n static def create_value_provider_instance(name, instance, is_builtin)\n return _class(name, _class.TYPE_VALUE_PROVIDER, instance, is_builtin)\n end\n \n # Create a symbol entry for an animation constructor (built-in like solid, breathe)\n static def create_animation_constructor(name, instance, is_builtin)\n return _class(name, _class.TYPE_ANIMATION_CONSTRUCTOR, instance, is_builtin)\n end\n \n # Create a symbol entry for an animation instance (user-defined)\n static def create_animation_instance(name, instance, is_builtin)\n return _class(name, _class.TYPE_ANIMATION, instance, is_builtin)\n end\n \n # Create a symbol entry for a color constructor (built-in like color_cycle, breathe_color)\n static def create_color_constructor(name, instance, is_builtin)\n return _class(name, _class.TYPE_COLOR_CONSTRUCTOR, instance, is_builtin)\n end\n \n # Create a symbol entry for a color instance (user-defined)\n static def create_color_instance(name, instance, is_builtin)\n return _class(name, _class.TYPE_COLOR, instance, is_builtin)\n end\n \n # Create a symbol entry for a variable\n static def create_variable(name, is_builtin)\n return _class(name, _class.TYPE_VARIABLE, nil, is_builtin)\n end\n \n # Create a symbol entry for a sequence\n static def create_sequence(name, is_builtin)\n return _class(name, _class.TYPE_SEQUENCE, nil, is_builtin)\n end\n \n # Create a symbol entry for a template\n static def create_template(name, is_builtin)\n return _class(name, _class.TYPE_TEMPLATE, nil, is_builtin)\n end\nend\n\n# Mock engine class for parameter validation during transpilation\nclass MockEngine\n var time_ms\n var strip_length\n \n def init()\n self.time_ms = 0\n self.strip_length = 30 # Default strip length for validation\n end\n \n def get_strip_length()\n return self.strip_length\n end\n\n def add(obj)\n return true\n end\nend\n\n# Enhanced symbol table class for holistic symbol management and caching\nclass SymbolTable\n var entries # Map of name -> SymbolEntry\n var mock_engine # MockEngine for validation\n \n def init()\n import animation_dsl\n self.entries = {}\n self.mock_engine = animation_dsl.MockEngine()\n end\n \n # Dynamically detect and cache symbol type when first encountered\n def _detect_and_cache_symbol(name)\n import animation_dsl\n if self.entries.contains(name)\n return self.entries[name] # Already cached\n end\n \n try\n import introspect\n \n # Check for named colors first (from animation_dsl.named_colors)\n if animation_dsl.named_colors.contains(name)\n var entry = animation_dsl._symbol_entry.create_color_instance(name, nil, true) # true = is_builtin\n self.entries[name] = entry\n return entry\n end\n \n # Check for special built-in functions like 'log'\n if name == \"log\"\n var entry = animation_dsl._symbol_entry.create_user_function(\"log\", true) # true = is_builtin\n self.entries[name] = entry\n return entry\n end\n \n \n # Check for user functions (they might not be in animation module directly)\n if animation.is_user_function(name)\n var entry = animation_dsl._symbol_entry.create_user_function(name, true)\n self.entries[name] = entry\n return entry\n end\n \n # Check for math functions (they are in animation._math, not directly in animation)\n if introspect.contains(animation._math, name)\n var entry = animation_dsl._symbol_entry.create_math_function(name, true)\n self.entries[name] = entry\n return entry\n end\n \n # Check if it exists in animation module\n if introspect.contains(animation, name)\n var obj = animation.(name)\n var obj_type = type(obj)\n\n # Detect palette objects (bytes() instances)\n if isinstance(obj, bytes)\n var entry = animation_dsl._symbol_entry.create_palette_constant(name, obj, true)\n self.entries[name] = entry\n return entry\n end\n \n # Detect integer constants (like LINEAR, SINE, COSINE, etc.)\n if obj_type == \"int\"\n var entry = animation_dsl._symbol_entry.create_constant(name, obj, true)\n self.entries[name] = entry\n return entry\n end\n \n # Detect constructors (functions/classes that create instances)\n if obj_type == \"function\" || obj_type == \"class\"\n try\n var instance = obj(self.mock_engine)\n if isinstance(instance, animation.color_provider)\n # Color providers are a subclass of value providers, check them first\n var entry = animation_dsl._symbol_entry.create_color_constructor(name, instance, true)\n self.entries[name] = entry\n return entry\n elif animation.is_value_provider(instance)\n var entry = animation_dsl._symbol_entry.create_value_provider_constructor(name, instance, true)\n self.entries[name] = entry\n return entry\n elif isinstance(instance, animation.animation)\n var entry = animation_dsl._symbol_entry.create_animation_constructor(name, instance, true)\n self.entries[name] = entry\n return entry\n end\n except .. as e, msg\n # If instance creation fails, it might still be a valid function\n # but not a constructor we can validate\n end\n end\n end\n \n # If not found in animation module, return nil (will be handled as user-defined)\n return nil\n \n except .. as e, msg\n # If detection fails, return nil\n return nil\n end\n end\n \n # Add a symbol entry to the table (with conflict detection) - returns the entry\n def add(name, entry)\n # First check if there's a built-in symbol with this name\n var builtin_entry = self._detect_and_cache_symbol(name)\n if builtin_entry != nil && builtin_entry.type != entry.type\n raise \"symbol_redefinition_error\", f\"Cannot define '{name}' as {entry.type_to_string()} - it conflicts with built-in {builtin_entry.type_to_string()}\"\n end\n \n # Check existing user-defined symbols\n var existing = self.entries.find(name)\n if existing != nil\n # Check if it's the same type\n if existing.type != entry.type\n raise \"symbol_redefinition_error\", f\"Cannot redefine symbol '{name}' as {entry.type_to_string()} - it's already defined as {existing.type_to_string()}\"\n end\n # If same type, allow update (for cases like reassignment)\n end\n \n self.entries[name] = entry\n return entry\n end\n \n # Check if a symbol exists (with dynamic detection)\n def contains(name)\n if self.entries.contains(name)\n return true\n end\n \n # Try to detect and cache it\n var entry = self._detect_and_cache_symbol(name)\n return entry != nil\n end\n \n # Get a symbol entry (with dynamic detection)\n def get(name)\n var entry = self.entries.find(name)\n if entry != nil\n return entry\n end\n \n # Try to detect and cache it\n return self._detect_and_cache_symbol(name)\n end\n \n # Get symbol reference for code generation (with dynamic detection)\n def get_reference(name)\n import animation_dsl\n # Try to get from cache or detect dynamically (includes named colors)\n var entry = self.get(name)\n if entry != nil\n # For builtin color entries, return the actual color value directly\n if entry.is_builtin && entry.type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n var color_value = animation_dsl.named_colors[name]\n # Convert integer to hex string format for transpiler\n return f\"0x{color_value:08X}\"\n end\n return entry.get_reference()\n end\n \n # Default to user-defined format\n return f\"{name}_\"\n end\n \n # Check if symbol exists (including named colors, with dynamic detection)\n def symbol_exists(name)\n # Use proper discovery through _detect_and_cache_symbol via contains()\n return self.contains(name)\n end\n \n # Create and register a palette instance symbol (user-defined)\n def create_palette(name, instance)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_palette_instance(name, instance, false)\n return self.add(name, entry)\n end\n \n # Create and register a color instance symbol (user-defined)\n def create_color(name, instance)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_color_instance(name, instance, false)\n return self.add(name, entry)\n end\n \n # Create and register an animation instance symbol (user-defined)\n def create_animation(name, instance)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_animation_instance(name, instance, false)\n return self.add(name, entry)\n end\n \n # Create and register a value provider instance symbol (user-defined)\n def create_value_provider(name, instance)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_value_provider_instance(name, instance, false)\n return self.add(name, entry)\n end\n \n # Create and register a variable symbol (user-defined)\n def create_variable(name)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_variable(name, false)\n return self.add(name, entry)\n end\n \n # Create and register a sequence symbol (user-defined)\n def create_sequence(name)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_sequence(name, false)\n return self.add(name, entry)\n end\n \n # Create and register a template symbol (user-defined)\n def create_template(name, param_types)\n import animation_dsl\n var entry = animation_dsl._symbol_entry.create_template(name, false)\n entry.set_param_types(param_types != nil ? param_types : {})\n return self.add(name, entry)\n end\n \n\n # Register a user function (detected at runtime)\n def register_user_function(name)\n import animation_dsl\n if !self.contains(name)\n var entry = animation_dsl._symbol_entry.create_user_function(name, false)\n self.add(name, entry)\n end\n end\n \n # Generic create function that can specify name/type/instance/builtin directly\n def create_generic(name, typ, instance, is_builtin)\n import animation_dsl\n var entry = animation_dsl._symbol_entry(name, typ, instance, is_builtin != nil ? is_builtin : false)\n return self.add(name, entry)\n end\n \n # Get the type of a symbol\n def get_type(name)\n var entry = self.get(name)\n return entry != nil ? entry.type_to_string() : nil\n end\n \n # Check if symbol takes arguments\n def takes_args(name)\n var entry = self.get(name)\n return entry != nil ? entry.takes_args : false\n end\n \n # Check if symbol takes positional arguments\n def takes_positional_args(name)\n var entry = self.get(name)\n return entry != nil ? entry.takes_positional_args() : false\n end\n \n # Check if symbol takes named arguments\n def takes_named_args(name)\n var entry = self.get(name)\n return entry != nil ? entry.takes_named_args() : false\n end\n \n # Get instance for validation\n def get_instance(name)\n var entry = self.get(name)\n return entry != nil ? entry.instance : nil\n end\n \n # Check if symbol is dangerous (creates new instances when called)\n def is_dangerous(name)\n var entry = self.get(name)\n return entry != nil ? entry.is_dangerous_call() : false\n end\n \n # Helper method to get named color value (uses proper discovery)\n def _get_named_color_value(color_name)\n import animation_dsl\n var entry = self.get(color_name) # This will trigger _detect_and_cache_symbol if needed\n if entry != nil && entry.is_builtin && entry.type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n var color_value = animation_dsl.named_colors[color_name]\n # Convert integer to hex string format for transpiler\n return f\"0x{color_value:08X}\"\n end\n return \"0xFFFFFFFF\" # Default fallback\n end\n \n # Debug method to list all symbols\n def list_symbols()\n var result = []\n for name : self.entries.keys()\n var entry = self.entries[name]\n result.push(f\"{name}: {entry.type_to_string()}\")\n end\n return result\n end\nend\n\n# Return module exports\nreturn {\n \"_symbol_entry\": SymbolEntry,\n \"_symbol_table\": SymbolTable,\n \"MockEngine\": MockEngine\n}"; modules["dsl/token.be"] = "# Token Types and Token Class for Animation DSL\n# Defines all token types and the Token class with line/column tracking\n\nclass Token\n # Basic token types\n # static var KEYWORD = 0 # strip, color, animation, sequence, etc.\n # static var IDENTIFIER = 1 # user-defined names\n # static var NUMBER = 2 # 123, 3.14\n # static var STRING = 3 # \"hello\", 'world'\n # static var COLOR = 4 # #FF0000, rgb(255,0,0), hsv(240,100,100)\n # static var TIME = 5 # 2s, 500ms, 1m, 2h\n # static var PERCENTAGE = 6 # 50%, 100%\n # static var MULTIPLIER = 7 # 2x, 0.5x\n \n # Human readable type name for each type value\n static var names = [\n \"KEYWORD\", \"IDENTIFIER\", \"NUMBER\", \"STRING\", \"COLOR\", \"TIME\", \"PERCENTAGE\", \"MULTIPLIER\",\n \"ASSIGN\", \"PLUS\", \"MINUS\", \"MULTIPLY\", \"DIVIDE\", \"MODULO\", \"POWER\",\n \"EQUAL\", \"NOT_EQUAL\", \"LESS_THAN\", \"LESS_EQUAL\", \"GREATER_THAN\", \"GREATER_EQUAL\",\n \"LOGICAL_AND\", \"LOGICAL_OR\", \"LOGICAL_NOT\",\n \"LEFT_PAREN\", \"RIGHT_PAREN\", \"LEFT_BRACE\", \"RIGHT_BRACE\", \"LEFT_BRACKET\", \"RIGHT_BRACKET\",\n \"COMMA\", \"SEMICOLON\", \"COLON\", \"DOT\", \"ARROW\",\n \"NEWLINE\", \"VARIABLE_REF\", \"COMMENT\", \"\" #-ex-EOF-#, \"ERROR\",\n \"EVENT_ON\", \"EVENT_INTERRUPT\", \"EVENT_RESUME\", \"EVENT_AFTER\"\n ]\n \n static var statement_keywords = [\n \"strip\", \"set\", \"color\", \"palette\", \"animation\", \n \"sequence\", \"function\", \"on\", \"run\", \"template\", \"param\", \"import\", \"berry\"\n ]\n \n static var keywords = [\n # Configuration keywords\n \"strip\", \"set\", \"import\", \"berry\", \"extern\",\n \n # Definition keywords\n \"color\", \"palette\", \"animation\", \"sequence\", \"function\", \"template\", \"param\", \"type\",\n \n # Control flow keywords\n \"play\", \"for\", \"with\", \"repeat\", \"times\", \"forever\", \"if\", \"else\", \"elif\",\n \"choose\", \"random\", \"on\", \"run\", \"wait\", \"goto\", \"interrupt\", \"resume\",\n \"while\", \"from\", \"to\", \"return\", \"reset\", \"restart\", \"every\",\n \n # Boolean and special values\n \"true\", \"false\", \"nil\", \"transparent\",\n \n # Event keywords\n \"startup\", \"shutdown\", \"button_press\", \"button_hold\", \"motion_detected\",\n \"brightness_change\", \"timer\", \"time\", \"sound_peak\", \"network_message\",\n \n # Time and measurement keywords\n \"ms\", \"s\", \"m\", \"h\"\n ]\n \n static var color_names = [\n \"red\", \"green\", \"blue\", \"white\", \"black\", \"yellow\", \"orange\", \"purple\",\n \"pink\", \"cyan\", \"magenta\", \"gray\", \"grey\", \"silver\", \"gold\", \"brown\",\n \"lime\", \"navy\", \"olive\", \"maroon\", \"teal\", \"aqua\", \"fuchsia\", \"indigo\",\n \"violet\", \"crimson\", \"coral\", \"salmon\", \"khaki\", \"plum\", \"orchid\",\n \"turquoise\", \"tan\", \"beige\", \"ivory\", \"snow\", \"transparent\"\n ]\n \n # # Operators\n # static var ASSIGN = 8 # =\n # static var PLUS = 9 # +\n # static var MINUS = 10 # -\n # static var MULTIPLY = 11 # *\n # static var DIVIDE = 12 # /\n # static var MODULO = 13 # %\n # static var POWER = 14 # ^\n \n # # Comparison operators\n # static var EQUAL = 15 # ==\n # static var NOT_EQUAL = 16 # !=\n # static var LESS_THAN = 17 # <\n # static var LESS_EQUAL = 18 # <=\n # static var GREATER_THAN = 19 # >\n # static var GREATER_EQUAL = 20 # >=\n \n # # Logical operators\n # static var LOGICAL_AND = 21 # &&\n # static var LOGICAL_OR = 22 # ||\n # static var LOGICAL_NOT = 23 # !\n \n # # Delimiters\n # static var LEFT_PAREN = 24 # (\n # static var RIGHT_PAREN = 25 # )\n # static var LEFT_BRACE = 26 # {\n # static var RIGHT_BRACE = 27 # }\n # static var LEFT_BRACKET = 28 # [\n # static var RIGHT_BRACKET = 29 # ]\n \n # # Separators\n # static var COMMA = 30 # ,\n # static var SEMICOLON = 31 # ;\n # static var COLON = 32 # :\n # static var DOT = 33 # .\n # static var ARROW = 34 # ->\n \n # # Special tokens\n # static var NEWLINE = 35 # \\n (significant in some contexts)\n # static var VARIABLE_REF = 36 # $identifier\n # static var COMMENT = 37 # # comment text\n # # static var EOF = 38 # End of file (REMOVED - reserved number)\n # static var ERROR = 39 # Error token for invalid input\n \n # # Event-related tokens\n # static var EVENT_ON = 40 # on (event handler keyword)\n # static var EVENT_INTERRUPT = 41 # interrupt\n # static var EVENT_RESUME = 42 # resume\n # static var EVENT_AFTER = 43 # after (for resume timing)\n \n var type # int - the type of this token (Token.KEYWORD, Token.IDENTIFIER, etc.)\n var value # String - the actual text value of the token\n var line # Integer - line number where token appears (1-based)\n var column # Integer - column number where token starts (1-based)\n var length # Integer - length of the token in characters\n \n # Initialize a new token\n #\n # @param type: int - Token type constant (Token.KEYWORD, Token.IDENTIFIER, etc.)\n # @param value: string - The actual text value\n # @param line: int - Line number (1-based)\n # @param column: int - Column number (1-based)\n # @param length: int - Length of token in characters (optional, defaults to value length)\n def init(typ, value, line, column, length)\n self.type = typ\n self.value = value != nil ? value : \"\"\n self.line = line != nil ? line : 1\n self.column = column != nil ? column : 1\n self.length = length != nil ? length : size(self.value)\n end\n \n # Get a string representation of the token for debugging\n #\n # @return string - Human-readable token description\n def tostring()\n var type_name = \"UNKNOWN\"\n if self.type >= 0 && self.type < size(self.names)\n type_name = self.names[self.type]\n end\n # if self.type == 38 #-self.EOF-#\n # return f\"Token({type_name} at {self.line}:{self.column})\"\n if self.type == 35 #-self.NEWLINE-#\n return f\"Token({type_name} at {self.line}:{self.column})\"\n elif size(self.value) > 20\n var short_value = self.value[0..17] + \"...\"\n return f\"Token({type_name}, '{short_value}' at {self.line}:{self.column})\"\n else\n return f\"Token({type_name}, '{self.value}' at {self.line}:{self.column})\"\n end\n end\n \nend\n\n# Utility functions for token handling\n\n# Check if a string is a reserved keyword\n#\n# @param word: string - Word to check\n# @return bool - True if word is a reserved keyword\ndef is_keyword(word)\n import animation_dsl\n for keyword : animation_dsl.Token.keywords\n if word == keyword\n return true\n end\n end\n return false\nend\n\n# Check if a string is a predefined color name\n#\n# @param word: string - Word to check\n# @return bool - True if word is a predefined color name\ndef is_color_name(word)\n import animation_dsl\n for color : animation_dsl.Token.color_names\n if word == color\n return true\n end\n end\n return false\nend\n\nreturn {\n \"Token\": Token,\n \"is_keyword\": is_keyword,\n \"is_color_name\": is_color_name\n}"; modules["dsl/transpiler.be"] = "# Ultra-Simplified DSL Transpiler for Animation DSL\n# Single-pass transpiler with minimal complexity\n# Leverages Berry's runtime for symbol resolution\n\nclass SimpleDSLTranspiler\n var pull_lexer # Pull lexer instance\n var output # Generated Berry code lines\n var warnings # Compilation warnings\n var run_statements # Collect all run statements for single engine.run()\n var strip_initialized # Track if strip was initialized\n var symbol_table # Enhanced symbol cache: name -> {type, instance, class_obj}\n var indent_level # Track current indentation level for nested sequences\n var has_template_calls # Track if we have template calls to trigger engine.run()\n var template_animation_params # Set of parameter names when processing template animation body\n \n # Context constants for process_value calls\n static var CONTEXT_VARIABLE = 1\n static var CONTEXT_COLOR = 2\n static var CONTEXT_ANIMATION = 3\n static var CONTEXT_ARGUMENT = 4\n static var CONTEXT_PROPERTY = 5\n static var CONTEXT_REPEAT_COUNT = 6\n static var CONTEXT_ARRAY_ELEMENT = 7\n static var CONTEXT_TIME = 8\n static var CONTEXT_EXPRESSION = 9\n static var CONTEXT_GENERIC = 10\n static var CONTEXT_COLOR_PROVIDER = 11\n \n # Helper class to track expression metadata for closure detection\n static class ExpressionResult\n var expr # The expression string\n var has_dynamic # Boolean: true if contains dynamic content that may change over time, hence needs to wrap into a closure\n var has_dangerous # Boolean: true if contains dangerous code, i.e. code that creates new instances so it shouldn't be called at each tick but only at initialization\n var has_computation # Boolean: true if contains operators (computation)\n var return_type # Int: result type number from SymbolEntry constants\n var instance_for_validation # Instance object for validation (nil by default)\n \n def init(expr, has_dynamic, has_dangerous, has_computation, return_type, instance_for_validation)\n self.expr = (expr != nil) ? expr : \"\"\n self.has_dynamic = bool(has_dynamic)\n self.has_dangerous = bool(has_dangerous)\n self.has_computation = bool(has_computation)\n self.return_type = (return_type != nil) ? return_type : 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#\n self.instance_for_validation = instance_for_validation # nil by default\n end\n \n # Check if this expression needs closure/function wrapping\n def needs_closure()\n return self.has_dynamic\n end\n \n # String representation for debugging\n def tostring()\n var instance_str = (self.instance_for_validation != nil) ? f\"instance={classname(self.instance_for_validation)}\" : \"instance=nil\"\n # var type_str = self._type_to_string(self.return_type)\n # return f\"ExpressionResult(expr='{self.expr}', dynamic={self.has_dynamic}, dangerous={self.has_dangerous}, comp={self.has_computation}, type={type_str}, {instance_str})\"\n return f\"ExpressionResult(expr='{self.expr}', dynamic={self.has_dynamic}, dangerous={self.has_dangerous}, comp={self.has_computation}, type={self.return_type}, {instance_str})\"\n end\n \n # # Helper method to convert type number to string for debugging\n # def _type_to_string(type_num)\n # if type_num == 1 #-animation_dsl._symbol_entry.TYPE_PALETTE_CONSTANT-# return \"palette_constant\"\n # elif type_num == 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-# return \"palette\"\n # elif type_num == 3 #-animation_dsl._symbol_entry.TYPE_CONSTANT-# return \"constant\"\n # elif type_num == 4 #-animation_dsl._symbol_entry.TYPE_MATH_FUNCTION-# return \"math_function\"\n # elif type_num == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-# return \"user_function\"\n # elif type_num == 6 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER_CONSTRUCTOR-# return \"value_provider_constructor\"\n # elif type_num == 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-# return \"value_provider\"\n # elif type_num == 8 #-animation_dsl._symbol_entry.TYPE_ANIMATION_CONSTRUCTOR-# return \"animation_constructor\"\n # elif type_num == 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-# return \"animation\"\n # elif type_num == 10 #-animation_dsl._symbol_entry.TYPE_COLOR_CONSTRUCTOR-# return \"color_constructor\"\n # elif type_num == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-# return \"color\"\n # elif type_num == 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# return \"variable\"\n # elif type_num == 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-# return \"sequence\"\n # elif type_num == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-# return \"template\"\n # else return f\"unknown({type_num})\"\n # end\n # end\n \n # Static method to combine expression results\n # Takes an expression string and 1-2 ExpressionResult parameters (checks for nil)\n static def combine(expr_str, result1, result2)\n var has_dynamic = false\n var has_dangerous = false\n var has_computation = true # If we're combining, it means there's an operator\n var return_type = 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# # Default to variable for composite expressions\n \n # Combine flags from all non-nil results\n if result1 != nil\n has_dynamic = has_dynamic || result1.has_dynamic\n has_dangerous = has_dangerous || result1.has_dangerous\n has_computation = has_computation || result1.has_computation\n end\n \n if result2 != nil\n has_dynamic = has_dynamic || result2.has_dynamic\n has_dangerous = has_dangerous || result2.has_dangerous\n has_computation = has_computation || result2.has_computation\n end\n \n # Compute the new return type\n # For composite expressions (combining two results), typically revert to TYPE_VARIABLE\n # unless both operands are the same specific type\n if result1 != nil && result2 != nil\n # If both operands have the same specific type, preserve it\n if result1.return_type == result2.return_type && result1.return_type != 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#\n return_type = result1.return_type\n else\n # Different types or one is variable -> result is variable\n return_type = 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#\n end\n elif result1 != nil\n # Only one operand (unary operation) - preserve its type unless it's composite\n return_type = has_computation ? 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# : result1.return_type\n elif result2 != nil\n # Only one operand (unary operation) - preserve its type unless it's composite\n return_type = has_computation ? 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# : result2.return_type\n end\n \n return _class(expr_str, has_dynamic #-has_dynamic-#, has_dangerous #-has_dangerous-#, has_computation #-has_computation-#, return_type, nil)\n end\n \n # Create a simple literal result (no dynamic elements)\n static def literal(expr, return_type, instance_for_validation)\n return _class(expr, false #-has_dynamic-#, false #-has_dangerous-#, false #-has_computation-#, return_type, instance_for_validation)\n end\n \n # Create a function call result (dynamic=true, dangerous=true)\n static def function_call(expr, return_type, instance_for_validation)\n return _class(expr, true #-has_dynamic-#, false #-has_dangerous-#, false #-has_computation-#, return_type, instance_for_validation)\n end\n \n # Create a constructor call result (dynamic=false, dangerous=true)\n static def constructor_call(expr, return_type, instance_for_validation)\n return _class(expr, false #-has_dynamic-#, true #-has_dangerous-#, false #-has_computation-#, return_type, instance_for_validation)\n end\n \n # Create a variable reference result (dynamic=true, dangerous=false)\n static def variable_ref(expr, return_type, instance_for_validation)\n return _class(expr, true #-has_dynamic-#, false #-has_dangerous-#, false #-has_computation-#, return_type, instance_for_validation)\n end\n \n # Create a property access result (dynamic=true, dangerous=false)\n static def property_access(expr, return_type, instance_for_validation)\n return _class(expr, true #-has_dynamic-#, false #-has_dangerous-#, false #-has_computation-#, return_type, instance_for_validation)\n end\n end\n \n def init(pull_lexer)\n import animation_dsl\n \n # Only support pull lexer interface now\n self.pull_lexer = pull_lexer\n self.output = []\n self.warnings = [] # Separate array for warnings\n self.run_statements = []\n self.strip_initialized = false # Track if strip was initialized\n self.symbol_table = animation_dsl._symbol_table() # Enhanced symbol cache with built-in detection\n self.indent_level = 0 # Track current indentation level\n self.has_template_calls = false # Track if we have template calls\n self.template_animation_params = nil # Set of parameter names when processing template animation body\n \n # Note: Special functions like 'log' are now auto-discovered dynamically by the symbol table\n end\n \n # Get current indentation string\n def get_indent()\n return \" \" * (self.indent_level + 1) # Base indentation is 2 spaces\n end\n \n # Helper method to process simple value assignments with symbol table tracking\n # Consolidates duplicate code from process_color and process_animation\n def _process_simple_value_assignment(name, context, symbol_create_method)\n # Check if this is a simple identifier reference before processing\n var current_tok = self.current()\n var is_simple_identifier = (current_tok != nil && current_tok.type == 1 #-animation_dsl.Token.IDENTIFIER-# && \n (self.peek() == nil || self.peek().type != 24 #-animation_dsl.Token.LEFT_PAREN-#))\n var ref_name = is_simple_identifier ? current_tok.value : nil\n \n # Regular value assignment\n var value_result = self.process_value(context)\n var inline_comment = self.collect_inline_comment()\n self.add(f\"var {name}_ = {value_result.expr}{inline_comment}\")\n \n # If this is an identifier reference to another object in our symbol table,\n # add this name to the symbol table as well for compile-time validation\n if is_simple_identifier && ref_name != nil && self.symbol_table.contains(ref_name)\n var ref_entry = self.symbol_table.get(ref_name)\n # Only copy actual instances, not just markers\n if ref_entry != nil && ref_entry.instance != nil\n symbol_create_method(name, ref_entry.instance)\n else\n symbol_create_method(name, nil)\n end\n else\n # Add simple object to symbol table with a marker\n symbol_create_method(name, nil)\n end\n end\n \n # Helper method to process user function calls (user.function_name())\n def _process_user_function_call(func_name)\n # Check if this is a function call (user.function_name())\n if self.current() != nil && self.current().type == 24 #-LEFT_PAREN-#\n # This is a user function call: user.function_name()\n # Don't check for existence during transpilation - trust that function will be available at runtime\n \n # User functions use positional parameters with engine as first argument\n # In closure context, use engine parameter directly\n var args = self.process_function_arguments(true)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n return f\"animation.get_user_function('{func_name}')({full_args})\"\n else\n self.error(\"User functions must be called with parentheses: user.function_name()\")\n return \"nil\"\n end\n end\n \n # Helper method to unwrap animation.resolve() calls\n # Takes an expression like \"animation.resolve(strip_len_)\" and returns \"strip_len_\"\n # Returns nil if the expression doesn't match the pattern or if the unwrapped part isn't a valid identifier\n def _unwrap_resolve(expr)\n import string\n \n # Check if expression starts with \"animation.resolve(\" and ends with \")\"\n if string.find(expr, \"animation.resolve(\") == 0 && expr[-1] == ')'\n # Extract the content between parentheses\n var start_pos = size(\"animation.resolve(\")\n var end_pos = size(expr) - 1 # Position of the closing parenthesis\n var inner_expr = expr[start_pos..end_pos-1]\n \n # Check if the inner expression looks like a valid identifier\n # It should contain only letters, digits, and underscores, and not be empty\n if size(inner_expr) > 0 && self._is_valid_identifier(inner_expr)\n return inner_expr\n end\n end\n \n return nil\n end\n \n # Helper method to check if a string is a valid identifier\n def _is_valid_identifier(text)\n import string\n \n if size(text) == 0\n return false\n end\n \n # First character must be letter or underscore\n var first_char = text[0]\n if !((first_char >= 'a' && first_char <= 'z') || \n (first_char >= 'A' && first_char <= 'Z') || \n first_char == '_')\n return false\n end\n \n # Remaining characters must be letters, digits, or underscores\n for i: 1..size(text)-1\n var ch = text[i]\n if !((ch >= 'a' && ch <= 'z') || \n (ch >= 'A' && ch <= 'Z') || \n (ch >= '0' && ch <= '9') || \n ch == '_')\n return false\n end\n end\n \n return true\n end\n \n # Helper method to determine the return type of a function call\n def _determine_function_return_type(entry)\n if entry != nil\n if entry.type == 8 #-animation_dsl._symbol_entry.TYPE_ANIMATION_CONSTRUCTOR-# || entry.type == 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-#\n return 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-#\n elif entry.type == 10 #-animation_dsl._symbol_entry.TYPE_COLOR_CONSTRUCTOR-# || entry.type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n return 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n elif entry.type == 6 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER_CONSTRUCTOR-# || entry.type == 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-#\n return 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-#\n elif entry.type == 1 #-animation_dsl._symbol_entry.TYPE_PALETTE_CONSTANT-# || entry.type == 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-#\n return 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-#\n elif entry.type == 4 #-animation_dsl._symbol_entry.TYPE_MATH_FUNCTION-#\n return 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# # Math functions return numeric values\n elif entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-# || entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n return 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# # User functions and templates can return anything\n end\n end\n return 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# # Default fallback\n end\n \n # Helper method to create symbol entry based on return type number\n def _create_symbol_by_return_type(name, return_type, instance)\n if return_type == 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-#\n return self.symbol_table.create_animation(name, instance)\n elif return_type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n return self.symbol_table.create_color(name, instance)\n elif return_type == 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-#\n return self.symbol_table.create_value_provider(name, instance)\n elif return_type == 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-#\n return self.symbol_table.create_palette(name, instance)\n elif return_type == 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-#\n return self.symbol_table.create_sequence(name)\n elif return_type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n return self.symbol_table.create_template(name, nil)\n else # TYPE_VARIABLE or any other type\n return self.symbol_table.create_variable(name)\n end\n end\n \n # Helper method to determine the return type of a symbol reference\n def _determine_symbol_return_type(entry)\n if entry.type == 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-# || entry.type == 8 #-animation_dsl._symbol_entry.TYPE_ANIMATION_CONSTRUCTOR-#\n return 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-#\n elif entry.type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-# || entry.type == 10 #-animation_dsl._symbol_entry.TYPE_COLOR_CONSTRUCTOR-#\n return 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n elif entry.type == 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-# || entry.type == 6 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER_CONSTRUCTOR-#\n return 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-#\n elif entry.type == 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-# || entry.type == 1 #-animation_dsl._symbol_entry.TYPE_PALETTE_CONSTANT-#\n return 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-#\n elif entry.type == 3 #-animation_dsl._symbol_entry.TYPE_CONSTANT-#\n return 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# # Constants are numeric values\n elif entry.type == 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#\n return 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#\n elif entry.type == 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-#\n return 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-#\n elif entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n return 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n else\n return 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-# # Default fallback\n end\n end\n \n # Main transpilation method - single pass\n def transpile()\n try\n self.add(\"import animation\")\n self.add(\"\")\n \n # Single pass: process all statements\n while !self.at_end()\n self.process_statement()\n end\n \n # Generate single engine.run() call after all run statements\n self.generate_engine_run()\n \n # Add warnings as comments if any exist\n if self.has_warnings()\n self.add(\"\")\n self.add(\"# Compilation warnings:\")\n for warning : self.warnings\n self.add(f\"# {warning}\")\n end\n end\n \n return self.join_output()\n except .. as e, msg\n self.error(f\"Transpilation failed: {msg}\")\n end\n end\n \n # Transpile template animation body (for engine_proxy classes)\n # Similar to template body but uses self.add() instead of engine.add()\n def transpile_template_animation_body()\n try\n # Process all statements in template animation body until we hit the closing brace\n var brace_depth = 0\n while !self.at_end()\n var tok = self.current()\n \n # Check for template end condition\n if tok != nil && tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-# && brace_depth == 0\n # This is the closing brace of the template - stop processing\n break\n end\n \n # Track brace depth for nested braces\n if tok != nil && tok.type == 26 #-animation_dsl.Token.LEFT_BRACE-#\n brace_depth += 1\n elif tok != nil && tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-#\n brace_depth -= 1\n end\n \n self.process_statement()\n end\n \n # For template animations, use self.add() instead of engine.add()\n if size(self.run_statements) > 0\n for run_stmt : self.run_statements\n var obj_name = run_stmt[\"name\"]\n var comment = run_stmt[\"comment\"]\n # In template animations, use self.add() for engine_proxy\n self.add(f\"self.add({obj_name}_){comment}\")\n end\n end\n \n return self.join_output()\n except .. as e, msg\n self.error(f\"Template animation body transpilation failed: {msg}\")\n end\n end\n \n # Process statements - simplified approach\n def process_statement()\n var tok = self.current()\n if tok == nil # EOF token removed - nil indicates end of file\n return\n end\n \n # Handle comments - preserve them in generated code\n if tok.type == 37 #-animation_dsl.Token.COMMENT-#\n self.add(tok.value) # Add comment as-is to output\n self.next()\n return\n end\n \n # Skip whitespace (newlines)\n if tok.type == 35 #-animation_dsl.Token.NEWLINE-#\n self.next()\n return\n end\n \n # Handle keywords\n if tok.type == 0 #-animation_dsl.Token.KEYWORD-#\n if tok.value == \"strip\"\n # Strip directive is temporarily disabled but remains a reserved keyword\n self.error(\"'strip' directive is temporarily disabled. Strip configuration is handled automatically.\")\n self.skip_statement()\n return\n elif tok.value == \"template\"\n # Only \"template animation\" is supported\n var next_tok = self.peek()\n if next_tok != nil && next_tok.type == 0 #-animation_dsl.Token.KEYWORD-# && next_tok.value == \"animation\"\n self.process_template_animation()\n else\n self.error(\"Simple 'template' is not supported. Use 'template animation' instead to create reusable animation classes.\")\n self.skip_statement()\n end\n else\n # For any other statement, ensure strip is initialized\n if !self.strip_initialized\n self.generate_default_strip_initialization()\n end\n \n if tok.value == \"color\"\n self.process_color()\n elif tok.value == \"palette\"\n self.process_palette()\n elif tok.value == \"animation\"\n self.process_animation()\n elif tok.value == \"set\"\n self.process_set()\n elif tok.value == \"sequence\"\n self.process_sequence()\n elif tok.value == \"run\"\n self.process_run()\n elif tok.value == \"import\"\n self.process_import()\n elif tok.value == \"on\"\n self.process_event_handler()\n elif tok.value == \"berry\"\n self.process_berry_code_block()\n elif tok.value == \"extern\"\n self.process_external_function()\n else\n self.error(f\"Unknown keyword '{tok.value}'.\")\n self.skip_statement()\n end\n end\n elif tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n # For property assignments, ensure strip is initialized\n if !self.strip_initialized\n self.generate_default_strip_initialization()\n end\n \n # Check if this is a log function call\n if tok.value == \"log\" && self.peek() != nil && self.peek().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n self.process_standalone_log()\n else\n # Check if this is a property assignment (identifier.property = value)\n self.process_property_assignment()\n end\n else\n self.error(f\"Unexpected token '{tok.value}'.\")\n self.skip_statement()\n end\n end\n \n # Process color definition: color red = #FF0000 or color cycle_red = color_cycle(palette=[red, blue])\n def process_color()\n self.next() # skip 'color'\n var name = self.expect_identifier()\n \n # Validate that the color name is not reserved\n if !self.validate_user_name(name, \"color\")\n self.skip_statement()\n return\n end\n \n self.expect_assign()\n \n # Check if this is a function call with named arguments (color provider)\n var tok = self.current()\n if (tok.type == 0 #-animation_dsl.Token.KEYWORD-# || tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#) && \n self.peek() != nil && self.peek().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n \n # This is a function call - check if it's a user function or built-in color provider\n var func_name = tok.value\n self.next() # consume function name\n \n var inline_comment = \"\"\n # Check for inline comment before opening paren\n if self.current() != nil && self.current().type == 37 #-animation_dsl.Token.COMMENT-#\n inline_comment = \" \" + self.current().value\n self.next()\n end\n \n # Get symbol table entry for this function\n var entry = self.symbol_table.get(func_name)\n \n # Check if this is a template call first\n if entry != nil && entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n # This is a template call - validate and process\n var args_str = self.process_function_arguments(false)\n \n # Validate template call arguments\n var provided_args = args_str != \"\" ? self._split_function_arguments(args_str) : []\n var template_info = entry.instance # This should contain parameter info\n if template_info != nil && template_info.contains(\"params\")\n var expected_params = template_info[\"params\"]\n var param_types = template_info.find(\"param_types\", {})\n \n if !self._validate_template_call_arguments(func_name, provided_args, expected_params, param_types)\n self.skip_statement()\n return\n end\n end\n \n var full_args = args_str != \"\" ? f\"engine, {args_str}\" : \"engine\"\n self.add(f\"var {name}_ = {func_name}_template({full_args}){inline_comment}\")\n \n # Register in symbol table as color instance\n self.symbol_table.create_color(name, nil)\n elif entry != nil && entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-#\n # This is a user function call - use positional parameters with engine as first argument\n var args = self.process_function_arguments(false)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n self.add(f\"var {name}_ = animation.get_user_function('{func_name}')({full_args}){inline_comment}\")\n \n # Track this symbol in our symbol table as a color instance (user function result)\n self.symbol_table.create_color(name, nil)\n else\n # Built-in functions use the new engine-first + named parameters pattern\n # Validate that the factory function exists at transpilation time\n if !self._validate_color_provider_factory_exists(func_name)\n self.error(f\"Color provider factory function '{func_name}' does not exist. Check the function name and ensure it's available in the animation module.\")\n self.skip_statement()\n return\n end\n \n # Generate the base function call immediately\n self.add(f\"var {name}_ = animation.{func_name}(engine){inline_comment}\")\n \n # Track this symbol in our symbol table\n var instance = self._create_instance_for_validation(func_name)\n if instance != nil\n self.symbol_table.create_color(name, instance)\n end\n \n # Process named arguments with validation\n self._process_named_arguments_for_color_provider(f\"{name}_\", func_name)\n end\n else\n # Use helper method to process simple value assignment\n self._process_simple_value_assignment(name, self.CONTEXT_COLOR, / name, instance -> self.symbol_table.create_color(name, instance))\n end\n end\n \n # Process palette definition: palette aurora_colors = [(0, #000022), (64, #004400), ...] or [red, 0x008000, blue, 0x112233]\n def process_palette()\n self.next() # skip 'palette'\n var name = self.expect_identifier()\n \n # Validate that the palette name is not reserved\n if !self.validate_user_name(name, \"palette\")\n self.skip_statement()\n return\n end\n \n self.expect_assign()\n \n # Expect array literal\n self.expect_left_bracket()\n var palette_entries = []\n var palette_comments = [] # Store comments for each entry\n \n # Detect syntax type by looking at the first entry\n self.skip_whitespace_including_newlines()\n \n if self.check_right_bracket()\n # Empty palette - not allowed\n self.error(\"Empty palettes are not allowed. A palette must contain at least one color entry.\")\n self.skip_statement()\n return\n end\n \n # Check if first entry starts with '(' (tuple syntax) or not (alternative syntax)\n var is_tuple_syntax = self.current() != nil && self.current().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n \n while !self.at_end() && !self.check_right_bracket()\n self.skip_whitespace_including_newlines()\n \n if self.check_right_bracket()\n break\n end\n \n if is_tuple_syntax\n # Parse tuple (value, color) - original syntax\n # Check if we accidentally have alternative syntax in tuple mode\n if self.current() != nil && self.current().type != 24 #-animation_dsl.Token.LEFT_PAREN-#\n self.error(\"Cannot mix alternative syntax [color1, color2, ...] with tuple syntax (value, color). Use only one syntax per palette.\")\n self.skip_statement()\n return\n end\n \n self.expect_left_paren()\n var value = self.expect_number()\n self.expect_comma()\n var color = self.process_palette_color() # Use specialized palette color processing\n self.expect_right_paren()\n \n # Convert to VRGB format entry and store as integer\n var vrgb_entry = self.convert_to_vrgb(value, color)\n var vrgb_int = int(f\"0x{vrgb_entry}\")\n palette_entries.push(vrgb_int)\n else\n # Parse color only - alternative syntax\n # Check if we accidentally have a tuple in alternative syntax mode\n if self.current() != nil && self.current().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n self.error(\"Cannot mix tuple syntax (value, color) with alternative syntax [color1, color2, ...]. Use only one syntax per palette.\")\n self.skip_statement()\n return\n end\n \n var color = self.process_palette_color() # Use specialized palette color processing\n\n # Convert to VRGB format entry and store as integer after setting alpha to 0xFF\n var vrgb_entry = self.convert_to_vrgb(0xFF, color)\n var vrgb_int = int(f\"0x{vrgb_entry}\")\n palette_entries.push(vrgb_int)\n end\n \n # Check for entry separator: comma OR newline OR end of palette\n # Also collect any comment that comes after the separator\n var entry_comment = \"\"\n \n if self.current() != nil && self.current().type == 30 #-animation_dsl.Token.COMMA-#\n self.next() # skip comma\n \n # Check for comment immediately after comma\n if self.current() != nil && self.current().type == 37 #-animation_dsl.Token.COMMENT-#\n entry_comment = self.current().value\n self.next()\n end\n \n # Skip remaining whitespace/newlines\n while !self.at_end()\n var tok = self.current()\n if tok != nil && tok.type == 35 #-animation_dsl.Token.NEWLINE-#\n self.next()\n else\n break\n end\n end\n elif self.current() != nil && self.current().type == 35 #-animation_dsl.Token.NEWLINE-#\n # Newline acts as entry separator - skip it and continue\n self.next() # skip newline\n self.skip_whitespace_including_newlines()\n elif !self.check_right_bracket()\n # For the last entry, check if there's a comment before the closing bracket\n if self.current() != nil && self.current().type == 37 #-animation_dsl.Token.COMMENT-#\n entry_comment = self.current().value\n self.next()\n elif !self.check_right_bracket()\n self.error(\"Expected ',' or ']' in palette definition\")\n break\n end\n end\n \n palette_comments.push(entry_comment) # Store comment (empty string if no comment)\n end\n \n self.expect_right_bracket()\n var inline_comment = self.collect_inline_comment()\n \n # Generate Berry bytes object with comments preserved\n # Check if we have any comments to preserve\n var has_comments = false\n for comment : palette_comments\n if comment != \"\"\n has_comments = true\n break\n end\n end\n \n if has_comments\n # Multi-line format with comments\n self.add(f\"var {name}_ = bytes({inline_comment}\")\n for i : 0..size(palette_entries)-1\n var hex_str = format(\"%08X\", palette_entries[i])\n var comment = palette_comments[i]\n var comment_suffix = comment != \"\" ? f\" {comment}\" : \"\"\n self.add(f\" \\\"{hex_str}\\\"{comment_suffix}\")\n end\n self.add(\")\")\n else\n # Single-line format (original behavior when no comments)\n var palette_data = \"\"\n for i : 0..size(palette_entries)-1\n if i > 0\n palette_data += \" \"\n end\n # Convert integer back to hex string for bytes() constructor\n var hex_str = format(\"%08X\", palette_entries[i])\n palette_data += f'\"{hex_str}\"'\n end\n \n self.add(f\"var {name}_ = bytes({palette_data}){inline_comment}\")\n end\n \n # Register palette in symbol table\n self.symbol_table.create_palette(name, nil)\n end\n \n # Process animation definition: animation pulse_red = pulse(color=red, period=2s)\n def process_animation()\n self.next() # skip 'animation'\n var name = self.expect_identifier()\n \n # Validate that the animation name is not reserved\n if !self.validate_user_name(name, \"animation\")\n self.skip_statement()\n return\n end\n \n self.expect_assign()\n \n # Check if this is a function call with named arguments\n var tok = self.current()\n if (tok.type == 0 #-animation_dsl.Token.KEYWORD-# || tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#) && \n self.peek() != nil && self.peek().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n \n # This is a function call - check if it's a user function or built-in\n var func_name = tok.value\n self.next() # consume function name\n \n var inline_comment = \"\"\n # Check for inline comment before opening paren\n if self.current() != nil && self.current().type == 37 #-animation_dsl.Token.COMMENT-#\n inline_comment = \" \" + self.current().value\n self.next()\n end\n \n # Get symbol table entry for this function\n var entry = self.symbol_table.get(func_name)\n \n # Check if this is a template call first\n if entry != nil && entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n # This is a template call - treat like user function\n var args = self.process_function_arguments(false)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n self.add(f\"var {name}_ = {func_name}_template({full_args}){inline_comment}\")\n \n # Register in symbol table as animation instance\n self.symbol_table.create_animation(name, nil)\n elif entry != nil && entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-#\n # This is a user function call - use positional parameters with engine as first argument\n var args = self.process_function_arguments(false)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n self.add(f\"var {name}_ = animation.get_user_function('{func_name}')({full_args}){inline_comment}\")\n \n # Track this symbol in our symbol table as animation instance (user function result)\n self.symbol_table.create_animation(name, nil)\n else\n # Built-in functions use the new engine-first + named parameters pattern\n # Validate that the factory function creates an animation instance at transpile time\n # Use symbol table's dynamic detection with type checking for animation constructors only\n if entry == nil || entry.type != 8 #-animation_dsl._symbol_entry.TYPE_ANIMATION_CONSTRUCTOR-#\n self.error(f\"Animation factory function '{func_name}' does not exist or does not create an instance of animation.animation class. Check the function name and ensure it returns an animation object.\")\n self.skip_statement()\n return\n end\n \n # Check if this is a template animation (user-defined, not built-in)\n if entry.is_builtin\n # Built-in animation constructor from animation module\n self.add(f\"var {name}_ = animation.{func_name}(engine){inline_comment}\")\n else\n # Template animation constructor (user-defined class)\n self.add(f\"var {name}_ = {func_name}_animation(engine){inline_comment}\")\n end\n \n # Track this symbol in our symbol table\n var instance = self._create_instance_for_validation(func_name)\n if instance != nil\n self.symbol_table.create_animation(name, instance)\n end\n \n # Process named arguments with validation\n self._process_named_arguments_for_animation(f\"{name}_\", func_name)\n end\n else\n # Use helper method to process simple value assignment\n self._process_simple_value_assignment(name, self.CONTEXT_ANIMATION, / name, instance -> self.symbol_table.create_animation(name, instance))\n end\n end\n \n # Process strip configuration: strip length 60\n # Temporarily disabled\n # def process_strip()\n # self.next() # skip 'strip'\n # var prop = self.expect_identifier()\n # if prop == \"length\"\n # var length = self.expect_number()\n # var inline_comment = self.collect_inline_comment()\n # self.add(f\"var engine = animation.init_strip({length}){inline_comment}\")\n # self.strip_initialized = true # Mark that strip was initialized\n # end\n # end\n \n # Process variable assignment: set brightness = 80%\n def process_set()\n self.next() # skip 'set'\n var name = self.expect_identifier()\n \n # Validate that the variable name is not reserved\n if !self.validate_user_name(name, \"variable\")\n self.skip_statement()\n return\n end\n \n self.expect_assign()\n \n var value_result = self.process_value(self.CONTEXT_VARIABLE)\n var inline_comment = self.collect_inline_comment()\n # Add to symbol table using appropriate method based on return type\n var local_entry = self._create_symbol_by_return_type(name, value_result.return_type, value_result.instance_for_validation)\n var local_ref = (local_entry != nil) ? local_entry.get_reference() : f\"{name}_\"\n self.add(f\"var {local_ref} = {value_result.expr}{inline_comment}\")\n end\n \n # Process template animation definition: template animation name { param ... }\n # Generates a class extending engine_proxy instead of a function\n def process_template_animation()\n self.next() # skip 'template'\n self.next() # skip 'animation'\n var name = self.expect_identifier()\n \n # Validate that the template animation name is not reserved\n if !self.validate_user_name(name, \"template animation\")\n self.skip_statement()\n return\n end\n \n self.expect_left_brace()\n \n # First pass: collect all parameters with validation\n var params = []\n var param_types = {}\n var param_names_seen = {} # Track duplicate parameter names\n \n while !self.at_end() && !self.check_right_brace()\n self.skip_whitespace_including_newlines()\n \n if self.check_right_brace()\n break\n end\n \n var tok = self.current()\n \n if tok != nil && tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"param\"\n # Process parameter declaration in template animation\n self.next() # skip 'param'\n var param_name = self.expect_identifier()\n \n # Validate parameter name (this is a template animation)\n if !self._validate_template_parameter_name(param_name, param_names_seen, true)\n self.skip_statement()\n return\n end\n \n # Parse parameter constraints (type, min, max, default)\n var param_constraints = self._parse_parameter_constraints()\n \n # Add parameter to collections\n params.push(param_name)\n param_names_seen[param_name] = true\n if param_constraints != nil && size(param_constraints) > 0\n param_types[param_name] = param_constraints\n end\n \n # Skip optional newline after parameter\n if self.current() != nil && self.current().type == 35 #-animation_dsl.Token.NEWLINE-#\n self.next()\n end\n else\n # Found non-param statement, break to collect body\n break\n end\n end\n \n # Generate Berry class for this template animation\n self.generate_template_animation_class(name, params, param_types)\n \n # Add template animation to symbol table with parameter information\n var template_info = {\n \"params\": params,\n \"param_types\": param_types\n }\n self.symbol_table.create_template(name, template_info)\n \n # Also register as an animation constructor so it can be used like: animation x = template_name(...)\n # We create a special entry that tracks it as both a template and an animation constructor\n self._register_template_animation_constructor(name, params, param_types)\n end\n \n # Process sequence definition: sequence demo { ... } or sequence demo repeat N times { ... }\n def process_sequence()\n self.next() # skip 'sequence'\n var name = self.expect_identifier()\n \n # Validate that the sequence name is not reserved\n if !self.validate_user_name(name, \"sequence\")\n self.skip_statement()\n return\n end\n \n # Track sequence in symbol table\n self.symbol_table.create_sequence(name)\n \n # Check for second syntax: sequence name repeat N times { ... } or sequence name forever { ... }\n var is_repeat_syntax = false\n var repeat_count = \"1\"\n \n var current_tok = self.current()\n if current_tok != nil && current_tok.type == 0 #-animation_dsl.Token.KEYWORD-#\n if current_tok.value == \"repeat\"\n is_repeat_syntax = true\n self.next() # skip 'repeat'\n \n # Parse repeat count: either number or \"forever\"\n var tok_after_repeat = self.current()\n if tok_after_repeat != nil && tok_after_repeat.type == 0 #-animation_dsl.Token.KEYWORD-# && tok_after_repeat.value == \"forever\"\n self.next() # skip 'forever'\n repeat_count = \"-1\" # -1 means forever\n else\n var count_result = self.process_value(self.CONTEXT_REPEAT_COUNT)\n self.expect_keyword(\"times\")\n repeat_count = count_result.expr\n end\n elif current_tok.value == \"forever\"\n # New syntax: sequence name forever { ... } (repeat is optional)\n is_repeat_syntax = true\n self.next() # skip 'forever'\n repeat_count = \"-1\" # -1 means forever\n end\n elif current_tok != nil && current_tok.type == 2 #-animation_dsl.Token.NUMBER-#\n # New syntax: sequence name N times { ... } (repeat is optional)\n is_repeat_syntax = true\n var count_result = self.process_value(self.CONTEXT_REPEAT_COUNT)\n self.expect_keyword(\"times\")\n repeat_count = count_result.expr\n end\n \n self.expect_left_brace()\n \n if is_repeat_syntax\n # Second syntax: sequence name repeat N times { ... }\n # Create a single sequence_manager with fluent interface\n self.add(f\"var {name}_ = animation.sequence_manager(engine, {repeat_count})\")\n \n # Process sequence body - add steps using fluent interface\n while !self.at_end() && !self.check_right_brace()\n self.process_sequence_statement()\n end\n else\n # First syntax: sequence demo { ... }\n # Use fluent interface for regular sequences too (no repeat count = default)\n self.add(f\"var {name}_ = animation.sequence_manager(engine)\")\n \n # Process sequence body - add steps using fluent interface\n while !self.at_end() && !self.check_right_brace()\n self.process_sequence_statement()\n end\n end\n \n self.expect_right_brace()\n end\n \n # Process statements inside sequences using fluent interface\n def process_sequence_statement()\n var tok = self.current()\n if tok == nil # EOF token removed - nil indicates end of file\n return\n end\n \n # Handle comments - preserve them in generated code with proper indentation\n if tok.type == 37 #-animation_dsl.Token.COMMENT-#\n self.add(self.get_indent() + tok.value) # Add comment with fluent indentation\n self.next()\n return\n end\n \n # Skip whitespace (newlines) - we specifically don't call skip_whitespace_including_newlines()\n if tok.type == 35 #-animation_dsl.Token.NEWLINE-#\n self.next()\n return\n end\n \n if tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"play\"\n self.process_play_statement_fluent()\n \n elif tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"wait\"\n self.process_wait_statement_fluent()\n \n elif tok.type == 1 #-animation_dsl.Token.IDENTIFIER-# && tok.value == \"log\"\n self.process_log_statement_fluent()\n \n elif tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"restart\"\n self.process_restart_statement_fluent()\n \n elif tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"repeat\"\n self.process_repeat_statement_fluent()\n \n elif tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"if\"\n self.process_if_statement_fluent()\n \n elif tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n # Check if this is a property assignment (identifier.property = value)\n if self.peek() != nil && self.peek().type == 33 #-animation_dsl.Token.DOT-#\n self.process_sequence_assignment_fluent()\n else\n # Unknown identifier in sequence - this is an error\n self.error(f\"Unknown command '{tok.value}' in sequence. Valid sequence commands are: play, wait, repeat, if, restart, log, or property assignments (object.property = value)\")\n self.skip_statement()\n end\n else\n # Unknown token type in sequence - this is an error\n self.error(f\"Invalid statement in sequence. Expected: play, wait, repeat, if, restart, log, or property assignments\")\n self.skip_statement()\n end\n end\n \n # Process property assignment using fluent style\n def process_sequence_assignment_fluent()\n var object_name = self.expect_identifier()\n self.expect_dot()\n var property_name = self.expect_identifier()\n self.expect_assign()\n var value_result = self.process_value(self.CONTEXT_PROPERTY)\n var inline_comment = self.collect_inline_comment()\n \n # Create assignment step using fluent style\n var closure_code = f\"def (engine) {object_name}_.{property_name} = {value_result.expr} end\"\n self.add(f\"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}\")\n end\n \n # Helper method to process play statement using fluent style\n def process_play_statement_fluent()\n self.next() # skip 'play'\n \n # Check if this is a function call or an identifier\n var anim_ref = \"\"\n var current_tok = self.current()\n if current_tok != nil && (current_tok.type == 1 #-animation_dsl.Token.IDENTIFIER-# || current_tok.type == 0 #-animation_dsl.Token.KEYWORD-#) &&\n self.peek() != nil && self.peek().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n # This is a function call - process it as a nested function call\n anim_ref = self.process_nested_function_call()\n else\n # This is an identifier reference\n var anim_name = self.expect_identifier()\n \n # Validate that the referenced object exists\n self._validate_object_reference(anim_name, \"sequence play\")\n \n anim_ref = f\"{anim_name}_\"\n end\n \n # Handle optional 'for duration'\n var duration = \"nil\"\n if self.current() != nil && self.current().type == 0 #-animation_dsl.Token.KEYWORD-# && self.current().value == \"for\"\n self.next() # skip 'for'\n var tok = self.current()\n \n # Check if duration is a literal time value or a variable reference\n if tok != nil && (tok.type == 5 #-animation_dsl.Token.TIME-# || tok.type == 2 #-animation_dsl.Token.NUMBER-#)\n # Literal time value - use directly\n duration = self.process_time_value()\n elif tok != nil && tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n # Variable reference - need to wrap in closure for dynamic values\n var duration_expr = self.process_time_value()\n # Check if this is a template animation parameter (starts with \"self.\")\n if duration_expr[0..4] == \"self.\"\n # Template animation parameter - wrap in closure for dynamic evaluation\n duration = f\"def (engine) return {duration_expr} end\"\n else\n # Regular variable - use directly (static value)\n duration = duration_expr\n end\n else\n duration = self.process_time_value()\n end\n end\n \n var inline_comment = self.collect_inline_comment()\n self.add(f\"{self.get_indent()}.push_play_step({anim_ref}, {duration}){inline_comment}\")\n end\n \n # Helper method to process wait statement using fluent style\n def process_wait_statement_fluent()\n self.next() # skip 'wait'\n var duration = self.process_time_value()\n var inline_comment = self.collect_inline_comment()\n self.add(f\"{self.get_indent()}.push_wait_step({duration}){inline_comment}\")\n end\n \n # Unified log processing method - handles all log contexts\n def process_log_call(args_str, context_type, inline_comment)\n # Convert DSL log(\"message\") to Berry log(f\"message\", 3)\n if context_type == \"fluent\"\n # For sequence context - wrap in closure\n var closure_code = f\"def (engine) log(f\\\"{args_str}\\\", 3) end\"\n return f\"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}\"\n elif context_type == self.CONTEXT_EXPRESSION\n # For expression context - return just the call (no inline comment)\n return f\"log(f\\\"{args_str}\\\", 3)\"\n else\n # For standalone context - direct call with comment\n return f\"log(f\\\"{args_str}\\\", 3){inline_comment}\"\n end\n end\n\n # Helper method to process log statement using fluent style\n def process_log_statement_fluent()\n self.next() # skip 'log'\n self.expect_left_paren()\n \n # Process the message string\n var message_tok = self.current()\n if message_tok == nil || message_tok.type != 3 #-animation_dsl.Token.STRING-#\n self.error(\"log() function requires a string message\")\n self.skip_statement()\n return\n end\n \n var message = message_tok.value\n self.next() # consume string\n self.expect_right_paren()\n \n var inline_comment = self.collect_inline_comment()\n # Use unified log processing\n var log_code = self.process_log_call(message, \"fluent\", inline_comment)\n self.add(log_code)\n end\n\n # Helper method to process restart statement using fluent style\n def process_restart_statement_fluent()\n var keyword = self.current().value # \"restart\"\n self.next() # skip 'restart'\n \n # Expect the value provider identifier\n var val_name = self.expect_identifier()\n \n # Validate that the value is a value_provider at transpile time\n if !self._validate_value_provider_reference(val_name, keyword)\n self.skip_statement()\n return\n end\n \n var inline_comment = self.collect_inline_comment()\n \n # Generate closure step that calls val.start(engine.time_ms)\n var closure_code = f\"def (engine) {val_name}_.start(engine.time_ms) end\"\n self.add(f\"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}\")\n end\n\n # Helper method to process repeat statement using fluent style\n def process_repeat_statement_fluent()\n self.next() # skip 'repeat'\n \n # Parse repeat count: either number or \"forever\"\n var repeat_count = \"1\"\n var tok_after_repeat = self.current()\n if tok_after_repeat != nil && tok_after_repeat.type == 0 #-animation_dsl.Token.KEYWORD-# && tok_after_repeat.value == \"forever\"\n self.next() # skip 'forever'\n repeat_count = \"-1\" # -1 means forever\n else\n var count_result = self.process_value(self.CONTEXT_REPEAT_COUNT)\n self.expect_keyword(\"times\")\n repeat_count = count_result.expr\n end\n \n self.expect_left_brace()\n \n # Create a nested sub-sequence using recursive processing\n self.add(f\"{self.get_indent()}.push_repeat_subsequence(animation.sequence_manager(engine, {repeat_count})\")\n \n # Increase indentation level for nested content\n self.indent_level += 1\n \n # Process repeat body recursively - just call the same method\n while !self.at_end() && !self.check_right_brace()\n self.process_sequence_statement()\n end\n \n self.expect_right_brace()\n \n # Decrease indentation level and close the sub-sequence\n self.add(f\"{self.get_indent()})\")\n self.indent_level -= 1\n end\n\n # Process if statement (conditional execution - runs 0 or 1 times based on boolean)\n def process_if_statement_fluent()\n self.next() # skip 'if'\n \n # Parse condition expression - use CONTEXT_EXPRESSION to avoid automatic function wrapping\n var condition_result = self.process_additive_expression(self.CONTEXT_EXPRESSION, true, false)\n \n self.expect_left_brace()\n \n # Create a nested sub-sequence with bool() wrapper to ensure 0 or 1 iterations\n # Check if expression is dynamic (needs closure) or static (can be evaluated directly)\n var repeat_count_expr\n if condition_result.has_dynamic\n # Dynamic expression - wrap in closure\n repeat_count_expr = f\"def (engine) return bool({condition_result.expr}) end\"\n else\n # Static expression - evaluate directly\n repeat_count_expr = f\"bool({condition_result.expr})\"\n end\n \n self.add(f\"{self.get_indent()}.push_repeat_subsequence(animation.sequence_manager(engine, {repeat_count_expr})\")\n \n # Increase indentation level for nested content\n self.indent_level += 1\n \n # Process if body recursively\n while !self.at_end() && !self.check_right_brace()\n self.process_sequence_statement()\n end\n \n self.expect_right_brace()\n \n # Decrease indentation level and close the sub-sequence\n self.add(f\"{self.get_indent()})\")\n self.indent_level -= 1\n end\n\n # Process import statement: import user_functions or import module_name\n def process_import()\n self.next() # skip 'import'\n var module_name = self.expect_identifier()\n \n var inline_comment = self.collect_inline_comment()\n \n # Generate Berry import statement with quoted module name\n self.add(f'import {module_name} {inline_comment}')\n end\n \n # Process standalone log statement: log(\"message\")\n def process_standalone_log()\n self.next() # skip 'log'\n self.expect_left_paren()\n \n # Process the message string\n var message_tok = self.current()\n if message_tok == nil || message_tok.type != 3 #-animation_dsl.Token.STRING-#\n self.error(\"log() function requires a string message\")\n self.skip_statement()\n return\n end\n \n var message = message_tok.value\n self.next() # consume string\n self.expect_right_paren()\n \n var inline_comment = self.collect_inline_comment()\n # Use unified log processing\n var log_code = self.process_log_call(message, \"standalone\", inline_comment)\n self.add(log_code)\n end\n \n # Process run statement: run demo\n def process_run()\n self.next() # skip 'run'\n var name = self.expect_identifier()\n \n # Validate that the referenced object exists\n self._validate_object_reference(name, \"run\")\n \n var inline_comment = self.collect_inline_comment()\n \n # Store run statement for later processing\n self.run_statements.push({\n \"name\": name,\n \"comment\": inline_comment\n })\n end\n \n # Process property assignment or standalone function call: animation_name.property = value OR template_call(args)\n def process_property_assignment()\n var object_name = self.expect_identifier()\n \n # Check if this is a function call (template call or special function)\n if self.current() != nil && self.current().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n # Special case for log function - allow as standalone\n if object_name == \"log\"\n var args = self.process_function_arguments(false)\n var inline_comment = self.collect_inline_comment()\n # Use unified log processing\n var log_code = self.process_log_call(args, \"standalone\", inline_comment)\n self.add(log_code)\n return\n end\n \n # This is a standalone function call - check if it's a template\n var entry = self.symbol_table.get(object_name)\n if entry != nil && entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n var args = self.process_function_arguments(false)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n var inline_comment = self.collect_inline_comment()\n self.add(f\"{object_name}_template({full_args}){inline_comment}\")\n \n # Track that we have template calls to trigger engine.run()\n self.has_template_calls = true\n else\n self.error(f\"Standalone function calls are only supported for templates. '{object_name}' is not a template.\")\n self.skip_statement()\n end\n return\n end\n \n # Check if next token is a dot (property assignment)\n if self.current() != nil && self.current().type == 33 #-animation_dsl.Token.DOT-#\n self.next() # skip '.'\n var property_name = self.expect_identifier()\n \n # Validate parameter if we have this object in our symbol table\n if self.symbol_table.contains(object_name)\n var entry = self.symbol_table.get(object_name)\n \n # Only validate parameters for actual instances, not sequence markers\n if entry != nil && entry.instance != nil\n var class_name = classname(entry.instance)\n \n # Use the existing parameter validation logic\n self._validate_single_parameter(class_name, property_name, entry.instance)\n elif entry != nil && entry.type == 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-#\n # This is a sequence marker - sequences don't have properties\n self.error(f\"Sequences like '{object_name}' do not have properties. Property assignments are only valid for animations and color providers.\")\n end\n end\n \n self.expect_assign()\n var value_result = self.process_value(self.CONTEXT_PROPERTY)\n var inline_comment = self.collect_inline_comment()\n \n # Use consolidated symbol resolution for property assignments\n var object_ref = self.symbol_table.get_reference(object_name)\n \n # Generate property assignment\n self.add(f\"{object_ref}.{property_name} = {value_result.expr}{inline_comment}\")\n else\n # Not a property assignment, skip this statement\n self.error(f\"Expected property assignment for '{object_name}' but found no dot\")\n self.skip_statement()\n end\n end\n \n # Process any value - unified approach\n def process_value(context)\n var result = self.process_additive_expression(context, true, false) # true = top-level, false = not raw mode\n # Handle closure wrapping for top-level expressions (not in raw mode) only if there is computation needed\n if (((context == self.CONTEXT_VARIABLE) || (context == self.CONTEXT_PROPERTY)) && result.needs_closure())\n || ((context == self.CONTEXT_REPEAT_COUNT) && result.needs_closure())\n # Special handling for repeat_count context - always create simple function for property access\n if context == self.CONTEXT_REPEAT_COUNT\n # print(f\">>> CONTEXT_REPEAT_COUNT\")\n var closure_expr = f\"def (engine) return {result.expr} end\"\n # Return new ExpressionResult with closure expression but preserve return type\n return self.ExpressionResult.function_call(closure_expr, result.return_type)\n else\n # Default behavior is to wrap into `animation.create_closure_value(engine, def (engine) return <> end)`\n var expr = f\"animation.create_closure_value(engine, def (engine) return {result.expr} end)\"\n\n if result.return_type == 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-# && !result.has_computation\n # Special case of a reference to another variable containing an animation, in such case no need for wrapping\n expr = result.expr\n end\n\n # Simple optimization, unwrap a single `animation.resolve()` instead of wrapping in a closure\n var unwrapped_expr = self._unwrap_resolve(result.expr)\n # print(f\"{unwrapped_expr=}\")\n if unwrapped_expr != nil\n expr = unwrapped_expr # override expr\n end\n\n # var closure_expr = self.create_computation_closure_from_string(result.expr)\n var entry_for_closure_value = self.symbol_table.get(\"closure_value\")\n return self.ExpressionResult.function_call(expr, entry_for_closure_value.type, entry_for_closure_value.instance)\n end\n else\n # Return the original result unchanged\n return result\n end\n end\n \n # Process palette color with strict validation\n # Only accepts predefined color names or hex color literals\n def process_palette_color()\n import animation_dsl\n var tok = self.current()\n if tok == nil\n self.error(\"Expected color value in palette\")\n return \"0xFFFFFFFF\"\n end\n \n # Handle hex color literals\n if tok.type == 4 #-animation_dsl.Token.COLOR-#\n self.next()\n return self.convert_color(tok.value)\n end\n \n # Handle identifiers (color names)\n if tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n var name = tok.value\n self.next()\n \n # Only accept predefined color names\n if animation_dsl.is_color_name(name)\n return self.get_named_color_value(name)\n end\n \n # Reject any other identifier\n self.error(f\"Unknown color '{name}'. Palettes only accept hex colors (0xRRGGBB) or predefined color names (like 'red', 'blue', 'green'), but not custom colors defined previously. For dynamic palettes with custom colors, use user functions instead.\")\n return \"0xFFFFFFFF\"\n end\n \n self.error(\"Expected color value in palette. Use hex colors (0xRRGGBB) or predefined color names (like 'red', 'blue', 'green').\")\n return \"0xFFFFFFFF\"\n end\n \n # Process additive expressions (+ and -) - unified method\n def process_additive_expression(context, is_top_level, raw_mode)\n var left_result = self.process_multiplicative_expression(context, is_top_level, raw_mode)\n \n while !self.at_end()\n var tok = self.current()\n if tok != nil && (tok.type == 9 #-animation_dsl.Token.PLUS-# || tok.type == 10 #-animation_dsl.Token.MINUS-#)\n var op = tok.value\n self.next() # consume operator\n var right_result = self.process_multiplicative_expression(context, false, raw_mode) # sub-expressions are not top-level\n\n # Check if either of left or right are dangerous calls, if so raise an error\n if left_result.has_dangerous || right_result.has_dangerous\n var dangerous_expr = left_result.has_dangerous ? left_result.expr : right_result.expr\n self.error(f\"Expression '{dangerous_expr}' cannot be used in computed expressions. This creates a new instance at each evaluation. Use either:\\n set var_name = {dangerous_expr}() # Single function call\\n set computed = (existing_var + 1) / 2 # Computation with existing values\")\n self.skip_statement()\n return self.ExpressionResult.literal(\"nil\")\n end\n\n left_result = self.ExpressionResult.combine(f\"{left_result.expr} {op} {right_result.expr}\", left_result, right_result)\n\n else\n break\n end\n end\n \n return left_result\n end\n \n # Process multiplicative expressions (* and /) - unified method\n def process_multiplicative_expression(context, is_top_level, raw_mode)\n var left_result = self.process_unary_expression(context, is_top_level, raw_mode)\n \n while !self.at_end()\n var tok = self.current()\n if tok != nil && (tok.type == 11 #-animation_dsl.Token.MULTIPLY-# || tok.type == 12 #-animation_dsl.Token.DIVIDE-#)\n var op = tok.value\n self.next() # consume operator\n var right_result = self.process_unary_expression(context, false, raw_mode) # sub-expressions are not top-level\n\n # Check if either of left or right are dangerous calls, if so raise an error\n if left_result.has_dangerous || right_result.has_dangerous\n var dangerous_expr = left_result.has_dangerous ? left_result.expr : right_result.expr\n self.error(f\"Expression '{dangerous_expr}' cannot be used in computed expressions. This creates a new instance at each evaluation. Use either:\\n set var_name = {dangerous_expr}() # Single function call\\n set computed = (existing_var + 1) / 2 # Computation with existing values\")\n self.skip_statement()\n return self.ExpressionResult.literal(\"nil\")\n end\n\n\n left_result = self.ExpressionResult.combine(f\"{left_result.expr} {op} {right_result.expr}\", left_result, right_result)\n else\n break\n end\n end\n \n return left_result\n end\n \n # Process unary expressions (- and +) - unified method\n def process_unary_expression(context, is_top_level, raw_mode)\n var tok = self.current()\n if tok == nil\n self.error(\"Expected value\")\n return self.ExpressionResult.literal(\"nil\")\n end\n \n # Handle unary minus for negative numbers\n if tok.type == 10 #-animation_dsl.Token.MINUS-#\n self.next() # consume the minus\n var expr_result = self.process_unary_expression(context, false, raw_mode) # sub-expressions are not top-level\n return self.ExpressionResult(f\"(-{expr_result.expr})\", expr_result.has_dynamic, expr_result.has_dangerous, true #-force has_computation-#, expr_result.return_type, expr_result.instance_for_validation)\n end\n \n # Handle unary plus (optional)\n if tok.type == 9 #-animation_dsl.Token.PLUS-#\n self.next() # consume the plus\n return self.process_unary_expression(context, false, raw_mode) # sub-expressions are not top-level\n end\n return self.process_primary_expression(context, is_top_level, raw_mode)\n end\n \n # Process primary expressions (literals, identifiers, function calls, parentheses) - unified method\n def process_primary_expression(context, is_top_level, raw_mode)\n var tok = self.current()\n if tok == nil\n self.error(\"Expected value\")\n return self.ExpressionResult.literal(\"nil\")\n end\n \n # Parenthesized expression\n if tok.type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n self.next() # consume '('\n var expr_result = self.process_additive_expression(context, false, raw_mode) # parenthesized expressions are not top-level\n self.expect_right_paren()\n return self.ExpressionResult(f\"({expr_result.expr})\", expr_result.has_dynamic, expr_result.has_dangerous, expr_result.has_computation, expr_result.return_type, expr_result.instance_for_validation)\n end\n \n # Color value\n if tok.type == 4 #-animation_dsl.Token.COLOR-#\n self.next()\n return self.ExpressionResult.literal(self.convert_color(tok.value), 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#)\n end\n \n # Time value\n if tok.type == 5 #-animation_dsl.Token.TIME-#\n return self.ExpressionResult.literal(self.process_time_value())\n end\n \n # Percentage value\n if tok.type == 6 #-animation_dsl.Token.PERCENTAGE-#\n return self.ExpressionResult.literal(str(self.process_percentage_value()))\n end\n \n # Number value\n if tok.type == 2 #-animation_dsl.Token.NUMBER-#\n var value = tok.value\n self.next()\n return self.ExpressionResult.literal(value)\n end\n \n # Boolean keywords\n if tok.type == 0 #-animation_dsl.Token.KEYWORD-# && (tok.value == \"true\" || tok.value == \"false\")\n var value = tok.value\n self.next()\n return self.ExpressionResult.literal(value)\n end\n \n # String value\n if tok.type == 3 #-animation_dsl.Token.STRING-#\n var value = tok.value\n self.next()\n return self.ExpressionResult.literal(f'\"{value}\"')\n end\n \n # Array literal (not supported in raw mode)\n if tok.type == 28 #-animation_dsl.Token.LEFT_BRACKET-# && !raw_mode\n var result = self.process_array_literal()\n return self.ExpressionResult.literal(result)\n end\n \n # Anthing that looks like a function call\n if (tok.type == 0 #-animation_dsl.Token.KEYWORD-# || tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#) && \n self.peek() != nil && self.peek().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n var func_name = tok.value\n var entry = self.symbol_table.get(func_name)\n \n # Check if the identifier exists\n if entry == nil\n self.error(f\"Unknown function or identifier '{func_name}'. Make sure it's defined before use.\")\n self.skip_statement()\n return self.ExpressionResult.literal(\"nil\")\n end\n \n # Special handling for user fonction function_name() calls (without 'user.' prefix)\n if entry.is_user_function()\n self.next()\n var result = self._process_user_function_call(func_name)\n return self.ExpressionResult.function_call(result)\n end\n\n # In raw mode, handle function calls differently\n if raw_mode\n self.next()\n \n # Check if this is a mathematical function\n if entry != nil && entry.type == 4 #-animation_dsl._symbol_entry.TYPE_MATH_FUNCTION-#\n var args = self.process_function_arguments(true)\n var result = self.ExpressionResult.function_call(f\"{entry.get_reference()}({args})\")\n end\n \n # Check if this is a template call\n if entry != nil && entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n var args = self.process_function_arguments(true)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n return self.ExpressionResult.function_call(f\"{func_name}_template({full_args})\")\n end\n \n # For other functions, this shouldn't happen in expression context\n self.error(f\"Function '{func_name}' not supported in expression context\")\n return self.ExpressionResult.literal(\"nil\")\n else\n # Regular mode - function calls are marked as having functions\n # Check if this is a simple function call first\n if !entry.takes_named_args()\n var result = self.process_function_call(context)\n var return_type = self._determine_function_return_type(entry)\n return self.ExpressionResult.function_call(result, return_type, entry.instance)\n # Check if this is a nested function call or a variable assignment with named parameters\n elif context == self.CONTEXT_ARGUMENT || context == self.CONTEXT_PROPERTY || context == self.CONTEXT_VARIABLE\n var result = self.process_nested_function_call()\n var return_type = self._determine_function_return_type(entry)\n return self.ExpressionResult.constructor_call(result, return_type, entry.instance)\n else\n var result = self.process_function_call(context)\n var return_type = self._determine_function_return_type(entry)\n return self.ExpressionResult.constructor_call(result, return_type, entry.instance)\n end\n end\n end\n \n # Identifier - could be color, animation, variable, or object property reference\n if tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n var name = tok.value\n \n # Check if this is a template animation parameter FIRST - before symbol table lookup\n # This allows template animation parameters to override any other symbol resolution\n if self.template_animation_params != nil && self.template_animation_params.contains(name)\n self.next()\n # This is a parameter in a template animation - return self.param reference\n # The wrapping in create_closure_value will be done at the assignment level, not here\n var param_ref = f\"self.{name}\"\n return self.ExpressionResult.variable_ref(param_ref, 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#, nil)\n end\n \n var entry = self.symbol_table.get(name)\n\n if entry == nil\n self.error(f\"Unknown identifier '{name}'. Make sure it's defined before use.\")\n self.skip_statement()\n return self.ExpressionResult.literal(\"nil\")\n end\n self.next()\n\n # Check if this is an object property reference (identifier.property)\n if self.current() != nil && self.current().type == 33 #-animation_dsl.Token.DOT-#\n self.next() # consume '.'\n var property_name = self.expect_identifier()\n \n # Property access - mark as having properties\n var property_expr = f\"{name}.{property_name}\"\n \n # Validate that the property exists on the referenced object (skip in raw mode)\n if !raw_mode && self.symbol_table.contains(name)\n # Only validate parameters for actual instances, not sequence markers\n if entry != nil && entry.instance != nil\n var class_name = classname(entry.instance)\n self._validate_single_parameter(class_name, property_name, entry.instance)\n elif entry != nil && entry.type == 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-#\n # This is a sequence marker - sequences don't have properties\n self.error(f\"Sequences like '{name}' do not have properties. Property references are only valid for animations and color providers.\")\n return self.ExpressionResult.literal(\"nil\")\n end\n end\n \n # Use consolidated symbol resolution for the object reference\n var object_ref = self.symbol_table.get_reference(name)\n \n return self.ExpressionResult.property_access(f\"{object_ref}.{property_name}\", \"variable\")\n end\n \n if entry.type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-# ||\n entry.type == 2 #-animation_dsl._symbol_entry.TYPE_PALETTE-# ||\n entry.type == 1 #-animation_dsl._symbol_entry.TYPE_PALETTE_CONSTANT-# ||\n entry.type == 3 #-animation_dsl._symbol_entry.TYPE_CONSTANT-#\n return self.ExpressionResult.literal(entry.get_reference(), 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#)\n end\n\n # Special handling for user functions used without parentheses\n if entry.is_user_function()\n # User function used without parentheses - call it with engine parameter\n var result = f\"animation.get_user_function('{name}')(engine)\"\n return self.ExpressionResult.function_call(result)\n end\n\n # Regular identifier - check if it's a variable reference\n var ref = self.symbol_table.get_reference(name)\n var return_type = self._determine_symbol_return_type(entry) # compute the return type based on entry\n if entry.type == 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-# ||\n entry.type == 12 #-animation_dsl._symbol_entry.TYPE_VARIABLE-#\n # Special case for simple value providers, wrap in animation.resolve()\n return self.ExpressionResult.function_call(f\"animation.resolve({ref})\", return_type)\n end\n return self.ExpressionResult.variable_ref(ref, return_type)\n end\n \n # Handle keywords that should be treated as identifiers (not sure this actually happens), 'run'\n if tok.type == 0 #-animation_dsl.Token.KEYWORD-#\n var name = tok.value\n self.next()\n return self.ExpressionResult.literal(f\"animation.{name}\")\n end\n \n self.error(f\"Unexpected value: {tok.value}\")\n self.skip_statement()\n return self.ExpressionResult.literal(\"nil\")\n end\n \n # Process function call (legacy - for non-animation contexts)\n def process_function_call(context)\n var tok = self.current()\n var func_name = \"\"\n \n # Handle both identifiers and keywords as function names\n if tok != nil && (tok.type == 1 #-animation_dsl.Token.IDENTIFIER-# || tok.type == 0 #-animation_dsl.Token.KEYWORD-#)\n func_name = tok.value\n self.next()\n else\n self.error(\"Expected function name\")\n return \"nil\"\n end\n \n # Check if this is a mathematical function - handle with positional arguments\n var entry = self.symbol_table.get(func_name)\n if entry != nil && entry.type == 4 #-animation_dsl._symbol_entry.TYPE_MATH_FUNCTION-#\n # Mathematical functions use positional arguments, not named parameters\n var args = self.process_function_arguments(false)\n return f\"{entry.get_reference()}({args})\"\n end\n \n # Special case for log function - call global log function directly\n if func_name == \"log\"\n var args = self.process_function_arguments(false)\n # Use unified log processing (return expression for use in contexts)\n return self.process_log_call(args, self.CONTEXT_EXPRESSION, \"\")\n end\n \n var args = self.process_function_arguments(false)\n \n # Check if it's a template call first\n if entry != nil && entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n # This is a template call - treat like user function\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n return f\"{func_name}_template({full_args})\"\n else\n # All functions are resolved from the animation module and need engine as first parameter\n if args != \"\"\n return f\"animation.{func_name}(engine, {args})\"\n else\n return f\"animation.{func_name}(engine)\"\n end\n end\n end\n \n # Process time value - simplified\n #\n # @Return string\n def process_time_value()\n var tok = self.current()\n if tok != nil && tok.type == 5 #-animation_dsl.Token.TIME-#\n var time_str = tok.value\n self.next()\n return str(self.convert_time_to_ms(time_str))\n elif tok != nil && tok.type == 2 #-animation_dsl.Token.NUMBER-#\n var num = tok.value\n self.next()\n return str(int(real(num)) * 1000) # assume seconds\n elif tok != nil && tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n # Handle variable references for time values\n var var_name = tok.value\n \n # Validate that the variable exists before processing\n self._validate_object_reference(var_name, \"duration\")\n \n var result = self.process_primary_expression(self.CONTEXT_TIME, true, false)\n return result.expr\n else\n self.error(\"Expected time value\")\n return \"1000\"\n end\n end\n \n # Process percentage value - simplified\n def process_percentage_value()\n var tok = self.current()\n if tok != nil && tok.type == 6 #-animation_dsl.Token.PERCENTAGE-#\n var percent_str = tok.value\n self.next()\n var percent = real(percent_str[0..-2])\n return int(percent * 255 / 100)\n elif tok != nil && tok.type == 2 #-animation_dsl.Token.NUMBER-#\n var num = tok.value\n self.next()\n return int(real(num))\n else\n self.error(\"Expected percentage value\")\n return 255\n end\n end\n \n # Helper methods - pull lexer only\n def current()\n return self.pull_lexer.peek_token()\n end\n \n def peek()\n return self.pull_lexer.peek_ahead(2) # Look ahead by 2 (next token after current)\n end\n \n def next()\n return self.pull_lexer.next_token()\n end\n \n def at_end()\n return self.pull_lexer.at_end()\n end\n \n def skip_whitespace()\n while !self.at_end()\n var tok = self.current()\n if tok != nil && (tok.type == 35 #-animation_dsl.Token.NEWLINE-# || tok.type == 37 #-animation_dsl.Token.COMMENT-#)\n self.next()\n else\n break\n end\n end\n end\n \n # Skip whitespace including newlines (for parameter parsing contexts)\n def skip_whitespace_including_newlines()\n while !self.at_end()\n var tok = self.current()\n if tok != nil && (tok.type == 37 #-animation_dsl.Token.COMMENT-# || tok.type == 35 #-animation_dsl.Token.NEWLINE-#)\n self.next()\n else\n break\n end\n end\n end\n \n # Collect inline comment if present and return it formatted for Berry code\n def collect_inline_comment()\n var tok = self.current()\n if tok != nil && tok.type == 37 #-animation_dsl.Token.COMMENT-#\n var comment = \" \" + tok.value # Add spacing before comment\n self.next()\n return comment\n end\n return \"\" # No comment found\n end\n \n def expect_identifier()\n var tok = self.current()\n if tok != nil && (tok.type == 1 #-animation_dsl.Token.IDENTIFIER-# || \n tok.type == 4 #-animation_dsl.Token.COLOR-# ||\n (tok.type == 0 #-animation_dsl.Token.KEYWORD-# && self.can_use_as_identifier(tok.value)))\n var name = tok.value\n self.next()\n return name\n else\n self.error(\"Expected identifier\")\n return \"unknown\"\n end\n end\n \n def can_use_as_identifier(keyword)\n # Keywords that can be used as identifiers in variable contexts\n var identifier_keywords = [\n # DSL keywords that might be used as parameter names or variable names\n \"color\", \"animation\", \"palette\",\n # Event names that can be used as identifiers\n \"startup\", \"shutdown\", \"button_press\", \"button_hold\", \"motion_detected\",\n \"brightness_change\", \"timer\", \"time\", \"sound_peak\", \"network_message\"\n ]\n \n for kw : identifier_keywords\n if keyword == kw\n return true\n end\n end\n return false\n end\n \n # Process function arguments with unified implementation\n # @param raw_mode: boolean - If true, returns raw expressions without closures (for expression contexts)\n # If false, processes values normally with closure wrapping (for statement contexts)\n def process_function_arguments(raw_mode)\n self.expect_left_paren()\n var args = []\n \n while !self.at_end() && !self.check_right_paren()\n self.skip_whitespace()\n \n if self.check_right_paren()\n break\n end\n \n var arg\n if raw_mode\n # For expression contexts - use raw mode to avoid closure wrapping\n var arg_result = self.process_additive_expression(self.CONTEXT_ARGUMENT, true, true) # raw_mode = true\n arg = arg_result.expr # Extract the expression string\n else\n # For statement contexts - use normal processing with closure wrapping\n var arg_result = self.process_value(self.CONTEXT_ARGUMENT)\n arg = arg_result.expr\n end\n args.push(arg)\n \n self.skip_whitespace()\n \n if self.current() != nil && self.current().type == 30 #-animation_dsl.Token.COMMA-#\n self.next() # skip comma\n self.skip_whitespace()\n elif !self.check_right_paren()\n self.error(\"Expected ',' or ')' in function arguments\")\n break\n end\n end\n \n self.expect_right_paren()\n \n # Join arguments with commas\n var result = \"\"\n for i : 0..size(args)-1\n if i > 0\n result += \", \"\n end\n result += args[i]\n end\n return result\n end\n \n # Process nested function call (generates temporary variable or raw expression)\n def process_nested_function_call()\n var tok = self.current()\n var func_name = \"\"\n \n # Handle both identifiers and keywords as function names\n if tok != nil && (tok.type == 1 #-animation_dsl.Token.IDENTIFIER-# || tok.type == 0 #-animation_dsl.Token.KEYWORD-#)\n func_name = tok.value\n self.next()\n else\n self.error(\"Expected function name\")\n return \"nil\"\n end\n var entry = self.symbol_table.get(func_name)\n \n # Check if this is a mathematical function - handle with positional arguments\n if entry != nil && entry.type == 4 #-animation_dsl._symbol_entry.TYPE_MATH_FUNCTION-#\n # Mathematical functions use positional arguments, not named parameters\n var args = self.process_function_arguments(true)\n return f\"{entry.get_reference()}({args})\" # Math functions are under _math namespace\n end\n \n # Special case for log function in nested calls\n if func_name == \"log\"\n var args = self.process_function_arguments(true)\n # Use unified log processing\n return self.process_log_call(args, self.CONTEXT_EXPRESSION, \"\")\n end\n \n # Check if this is a template call\n if entry != nil && entry.type == 14 #-animation_dsl._symbol_entry.TYPE_TEMPLATE-#\n # This is a template call - treat like user function\n var args = self.process_function_arguments(true)\n var full_args = args != \"\" ? f\"engine, {args}\" : \"engine\"\n return f\"{func_name}_template({full_args})\"\n else\n # TODO not sure we can go that far with a symbol not in animation\n if !self._validate_animation_factory_exists(func_name)\n self.error(f\"Animation factory function '{func_name}' does not exist. Check the function name and ensure it's available in the animation module.\")\n self.skip_function_arguments() # Skip the arguments to avoid parsing errors\n return \"nil\"\n end\n\n self.expect_left_paren()\n\n # lines contains the attriute assignments to put in the closure\n var lines = []\n \n # Use the core processing logic with a callback for anonymous function assignments\n var assignment_callback = def (param_name, param_value, inline_comment)\n lines.push(f\" provider.{param_name} = {param_value}{inline_comment}\")\n end\n \n self._process_parameters_core(func_name, \"generic\", assignment_callback)\n self.expect_right_paren()\n\n if size(lines) > 0\n # Join all lines into a single expression\n var result = \"\"\n for i : 0..size(lines)-1\n if i > 0\n result += \"\\n\"\n end\n result += lines[i]\n end\n\n return f\"(def (engine)\\n\"\n \" var provider = animation.{func_name}(engine)\\n\"\n \"{result}\\n\"\n \" return provider\\n\"\n \"end)(engine)\"\n else\n return f\"animation.{func_name}(engine)\"\n end\n end\n end\n \n def expect_assign()\n var tok = self.current()\n if tok != nil && tok.type == 8 #-animation_dsl.Token.ASSIGN-#\n self.next()\n else\n self.error(\"Expected '='\")\n end\n end\n \n def expect_left_paren()\n var tok = self.current()\n if tok != nil && tok.type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n self.next()\n else\n self.error(\"Expected '('\")\n end\n end\n \n def expect_right_paren()\n var tok = self.current()\n if tok != nil && tok.type == 25 #-animation_dsl.Token.RIGHT_PAREN-#\n self.next()\n else\n self.error(\"Expected ')'\")\n end\n end\n \n def check_right_paren()\n var tok = self.current()\n return tok != nil && tok.type == 25 #-animation_dsl.Token.RIGHT_PAREN-#\n end\n \n def expect_comma()\n var tok = self.current()\n if tok != nil && tok.type == 30 #-animation_dsl.Token.COMMA-#\n self.next()\n else\n self.error(\"Expected ','\")\n end\n end\n \n def expect_left_brace()\n var tok = self.current()\n if tok != nil && tok.type == 26 #-animation_dsl.Token.LEFT_BRACE-#\n self.next()\n else\n self.error(\"Expected '{'\")\n end\n end\n \n def expect_right_brace()\n var tok = self.current()\n if tok != nil && tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-#\n self.next()\n else\n self.error(\"Expected '}'\")\n end\n end\n \n def check_right_brace()\n var tok = self.current()\n return tok != nil && tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-#\n end\n \n def expect_number()\n var tok = self.current()\n if tok != nil && tok.type == 2 #-animation_dsl.Token.NUMBER-#\n var value = tok.value\n self.next()\n return value\n else\n self.error(\"Expected number\")\n return \"0\"\n end\n end\n \n def expect_keyword(keyword)\n var tok = self.current()\n if tok != nil && tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == keyword\n self.next()\n else\n self.error(f\"Expected '{keyword}'\")\n end\n end\n \n def expect_colon()\n var tok = self.current()\n if tok != nil && tok.type == 32 #-animation_dsl.Token.COLON-#\n self.next()\n else\n self.error(\"Expected ':'\")\n end\n end\n \n def expect_dot()\n var tok = self.current()\n if tok != nil && tok.type == 33 #-animation_dsl.Token.DOT-#\n self.next()\n else\n self.error(\"Expected '.'\")\n end\n end\n \n def expect_left_bracket()\n var tok = self.current()\n if tok != nil && tok.type == 28 #-animation_dsl.Token.LEFT_BRACKET-#\n self.next()\n else\n self.error(\"Expected '['\")\n end\n end\n \n def expect_right_bracket()\n var tok = self.current()\n if tok != nil && tok.type == 29 #-animation_dsl.Token.RIGHT_BRACKET-#\n self.next()\n else\n self.error(\"Expected ']'\")\n end\n end\n \n def check_right_bracket()\n var tok = self.current()\n return tok != nil && tok.type == 29 #-animation_dsl.Token.RIGHT_BRACKET-#\n end\n \n\n \n # Process array literal [item1, item2, item3]\n def process_array_literal()\n self.expect_left_bracket()\n var items = []\n \n while !self.at_end() && !self.check_right_bracket()\n # Process array element\n var item_result = self.process_value(self.CONTEXT_ARRAY_ELEMENT)\n items.push(item_result.expr)\n \n if self.current() != nil && self.current().type == 30 #-animation_dsl.Token.COMMA-#\n self.next() # skip comma\n elif !self.check_right_bracket()\n self.error(\"Expected ',' or ']' in array literal\")\n break\n end\n end\n \n self.expect_right_bracket()\n \n # Join items with commas and wrap in brackets\n var result = \"[\"\n for i : 0..size(items)-1\n if i > 0\n result += \", \"\n end\n result += items[i]\n end\n result += \"]\"\n return result\n end\n \n def skip_statement()\n # Skip to next statement (newline or EOF)\n while !self.at_end()\n var tok = self.current()\n if tok == nil || tok.type == 35 #-animation_dsl.Token.NEWLINE-# # EOF token removed - check nil\n break\n end\n self.next()\n end\n end\n \n # Skip function arguments when validation fails\n def skip_function_arguments()\n if self.current() != nil && self.current().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n self.next() # consume '('\n var paren_count = 1\n \n while !self.at_end() && paren_count > 0\n var tok = self.current()\n if tok.type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n paren_count += 1\n elif tok.type == 25 #-animation_dsl.Token.RIGHT_PAREN-#\n paren_count -= 1\n end\n self.next()\n end\n end\n end\n \n # Conversion helpers\n def convert_color(color_str)\n import animation_dsl\n import string\n # Handle 0x hex colors (new format)\n if string.startswith(color_str, \"0x\")\n if size(color_str) == 10 # 0xAARRGGBB (with alpha channel)\n return color_str\n elif size(color_str) == 8 # 0xRRGGBB (without alpha channel - add opaque alpha)\n return f\"0xFF{color_str[2..]}\"\n end\n end\n \n # Handle named colors - use framework's color name system\n if animation_dsl.is_color_name(color_str)\n return self.get_named_color_value(color_str)\n end\n \n # Unknown color - return white as default\n return \"0xFFFFFFFF\"\n end\n \n # Get the ARGB value for a named color\n # This should match the colors supported by is_color_name()\n def get_named_color_value(color_name)\n return self.symbol_table.get_reference(color_name)\n end\n \n # Validate that a user-defined name is not a predefined color or DSL keyword\n def validate_user_name(name, definition_type)\n import animation_dsl\n # Check if the name already exists in the symbol table\n var entry = self.symbol_table.get(name)\n if entry == nil\n # Name is available - continue with other checks\n elif entry.is_builtin && entry.type == 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#\n self.error(f\"Cannot redefine predefined color '{name}'. Use a different name like '{name}_custom' or 'my_{name}'\")\n return false\n elif entry.is_builtin\n self.error(f\"Cannot redefine built-in symbol '{name}'. Use a different name like '{name}_custom' or 'my_{name}'\")\n return false\n elif definition_type == \"extern function\" && entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-#\n # Allow duplicate extern function declarations for the same function\n return true\n else\n # User-defined symbol already exists - this is a redefinition error\n self.error(f\"Symbol '{name}' is already defined. Cannot redefine as {definition_type}.\")\n return false\n end\n \n # Check if it's a DSL statement keyword\n for keyword : animation_dsl.Token.statement_keywords\n if name == keyword\n self.error(f\"Cannot use DSL keyword '{name}' as {definition_type} name. Use a different name like '{name}_custom' or 'my_{name}'\")\n return false\n end\n end\n \n return true\n end\n \n # Convert palette entry to VRGB format (Value, Red, Green, Blue)\n # Used by palette definitions to create Berry bytes objects\n #\n # @param value: string - palette position value (0-255)\n # @param color: string - color value (hex format like \"0xFFRRGGBB\")\n # @return string - 8-character hex string in VRGB format\n def convert_to_vrgb(value, color)\n import string\n \n # Convert value to hex (2 digits)\n var val_int = int(real(value))\n if val_int < 0\n val_int = 0\n elif val_int > 255\n val_int = 255\n end\n var val_hex = string.format(\"%02X\", val_int)\n \n # Extract RGB from color\n var color_str = str(color)\n var rgb_hex = \"FFFFFF\" # Default to white\n \n if string.startswith(color_str, \"0x\") && size(color_str) >= 10\n # Extract RGB components (skip alpha channel)\n # Format is \"0xAARRGGBB\", we want \"RRGGBB\"\n rgb_hex = color_str[4..9] # Skip \"0xAA\" to get \"RRGGBB\"\n elif string.startswith(color_str, \"0x\") && size(color_str) == 8\n # Format is \"0xRRGGBB\", we want \"RRGGBB\" \n rgb_hex = color_str[2..7] # Skip \"0x\" to get \"RRGGBB\"\n end\n \n return val_hex + rgb_hex # VRRGGBB format\n end\n \n def convert_time_to_ms(time_str)\n import string\n if string.endswith(time_str, \"ms\")\n return int(real(time_str[0..-3]))\n elif string.endswith(time_str, \"s\")\n return int(real(time_str[0..-2]) * 1000)\n elif string.endswith(time_str, \"m\")\n return int(real(time_str[0..-2]) * 60000)\n elif string.endswith(time_str, \"h\")\n return int(real(time_str[0..-2]) * 3600000)\n end\n return 1000\n end\n \n def add(line)\n self.output.push(line)\n end\n \n def join_output()\n # Use list.concat() for O(n) performance instead of O(n\u00b2) string concatenation\n return self.output.concat(\"\\n\") + \"\\n\"\n end\n \n def error(msg)\n var line = self.current() != nil ? self.current().line : 0\n var error_msg = f\"Line {line}: {msg}\"\n raise \"dsl_compilation_error\", error_msg\n end\n \n def warning(msg)\n var line = self.current() != nil ? self.current().line : 0\n self.warnings.push(f\"Line {line}: {msg}\")\n end\n \n def get_warnings()\n return self.warnings\n end\n \n def has_warnings()\n return size(self.warnings) > 0\n end\n \n def get_symbol_table_report()\n import string\n \n var report = \"## Symbol Table\\n\\n\"\n \n var symbols = self.symbol_table.list_symbols()\n if size(symbols) == 0\n report += \"No symbols defined\\n\\n\"\n return report\n end\n \n # Helper function to calculate display width (accounting for Unicode characters)\n def display_width(s)\n # Common Unicode symbols and their display widths\n var unicode_widths = {\n \"\u2713\": 1, # Check mark\n \"\u26a0\ufe0f\": 2, # Warning sign (emoji) - actually displays as 2 characters wide\n \"\u26a0\": 1 # Warning sign (text)\n }\n \n var width = 0\n var i = 0\n while i < size(s)\n var found_unicode = false\n # Check for known Unicode symbols\n for symbol : unicode_widths.keys()\n if i + size(symbol) <= size(s) && s[i..i+size(symbol)-1] == symbol\n width += unicode_widths[symbol]\n i += size(symbol)\n found_unicode = true\n break\n end\n end\n \n if !found_unicode\n # Regular ASCII character\n width += 1\n i += 1\n end\n end\n \n return width\n end\n \n # Collect all symbol data first to calculate column widths\n var symbol_data = []\n var max_name_len = 6 # \"Symbol\"\n var max_type_len = 4 # \"Type\"\n var max_builtin_len = 7 # \"Builtin\"\n var max_dangerous_len = 9 # \"Dangerous\"\n var max_takes_args_len = 10 # \"Takes Args\"\n \n for symbol_info : symbols\n var parts = string.split(symbol_info, \": \")\n if size(parts) >= 2\n var name = parts[0]\n var typ = parts[1]\n var entry = self.symbol_table.get(name)\n \n # Filter out built-in colors to reduce noise\n if entry != nil\n var builtin = entry.is_builtin ? \"\u2713\" : \"\"\n var dangerous = entry.is_dangerous_call() ? \"\u26a0\ufe0f\" : \"\"\n var takes_args = entry.takes_args ? \" \u2713 \" : \"\"\n \n # Calculate max widths using display width\n var name_with_backticks = f\"`{name}`\"\n if display_width(name_with_backticks) > max_name_len\n max_name_len = display_width(name_with_backticks)\n end\n if display_width(typ) > max_type_len\n max_type_len = display_width(typ)\n end\n if display_width(builtin) > max_builtin_len\n max_builtin_len = display_width(builtin)\n end\n if display_width(dangerous) > max_dangerous_len\n max_dangerous_len = display_width(dangerous)\n end\n if display_width(takes_args) > max_takes_args_len\n max_takes_args_len = display_width(takes_args)\n end\n \n symbol_data.push({\n \"name\": name_with_backticks,\n \"typ\": typ,\n \"builtin\": builtin,\n \"dangerous\": dangerous,\n \"takes_args\": takes_args\n })\n end\n end\n end\n \n # Sort symbol_data by name (case-insensitive)\n def _sort_symbol_data()\n var n = size(symbol_data)\n if n <= 1\n return\n end\n \n # Insertion sort for small lists\n var i = 1\n while i < n\n var key = symbol_data[i]\n var key_name = key['name']\n var j = i\n while j > 0 && symbol_data[j-1]['name'] > key_name\n symbol_data[j] = symbol_data[j-1]\n j -= 1\n end\n symbol_data[j] = key\n i += 1\n end\n end\n \n _sort_symbol_data()\n \n # Helper function to pad strings to specific width (using display width)\n def pad_string(s, width)\n var padding = width - display_width(s)\n if padding <= 0\n return s\n end\n return s + (\" \" * padding)\n end\n \n def center_string(s, width)\n var padding = width - display_width(s)\n if padding <= 0\n return s\n end\n var left_pad = padding / 2\n var right_pad = padding - left_pad\n return (\" \" * left_pad) + s + (\" \" * right_pad)\n end\n \n # Create properly formatted table header\n var header = f\"| {pad_string('Symbol', max_name_len)} | {pad_string('Type', max_type_len)} | {pad_string('Builtin', max_builtin_len)} | {pad_string('Dangerous', max_dangerous_len)} | {pad_string('Takes Args', max_takes_args_len)} |\\n\"\n var separator = f\"|{'-' * (max_name_len + 2)}|{'-' * (max_type_len + 2)}|{'-' * (max_builtin_len + 2)}|{'-' * (max_dangerous_len + 2)}|{'-' * (max_takes_args_len + 2)}|\\n\"\n \n report += header\n report += separator\n \n # Add formatted rows\n for data : symbol_data\n var row = f\"| {pad_string(data['name'], max_name_len)} | {pad_string(data['typ'], max_type_len)} | {center_string(data['builtin'], max_builtin_len)} | {center_string(data['dangerous'], max_dangerous_len)} | {center_string(data['takes_args'], max_takes_args_len)} |\\n\"\n report += row\n end\n \n report += \"\\n\"\n return report\n end\n\n def get_error_report()\n var report = \"\"\n \n if self.has_warnings()\n report += \"Compilation warnings:\\n\"\n for warning : self.warnings\n report += \" \" + warning + \"\\n\"\n end\n end\n \n if report == \"\"\n return \"No compilation warnings\"\n end\n \n return report\n end\n \n # Generate single engine.run() call for all run statements\n def generate_engine_run()\n if size(self.run_statements) == 0 && !self.has_template_calls\n return # No run statements or template calls, no need to start engine\n end\n \n # Add all animations/sequences to the engine\n for run_stmt : self.run_statements\n var name = run_stmt[\"name\"]\n var comment = run_stmt[\"comment\"]\n \n # Check if this is a sequence or regular animation\n # Use unified add() method - it will detect the type automatically\n self.add(f\"engine.add({name}_){comment}\")\n end\n \n # Single engine.run() call\n self.add(\"engine.run()\")\n end\n\n # Basic event handler processing\n def process_event_handler()\n self.next() # skip 'on'\n var event_name = self.expect_identifier()\n var line = self.current() != nil ? self.current().line : 0\n \n # Check for event parameters (e.g., timer(5s))\n var event_params = \"{}\"\n if self.current() != nil && self.current().type == 24 #-animation_dsl.Token.LEFT_PAREN-#\n event_params = self.process_event_parameters()\n end\n \n self.expect_colon()\n \n # Generate unique handler function name\n var handler_name = f\"event_handler_{event_name}_{line}\"\n \n # Start generating the event handler function\n self.add(f\"def {handler_name}(event_data)\")\n \n # Process the event action - simple function call or identifier\n var tok = self.current()\n if tok != nil\n if tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == \"interrupt\"\n self.next() # skip 'interrupt'\n var target = self.expect_identifier()\n if target == \"current\"\n self.add(\" engine.interrupt_current()\")\n else\n self.add(f\" engine.interrupt_animation(\\\"{target}\\\")\")\n end\n else\n # Assume it's an animation function call or reference\n var action_result = self.process_value(self.CONTEXT_ANIMATION)\n self.add(f\" engine.add({action_result.expr})\")\n end\n end\n \n self.add(\"end\")\n \n # Register the event handler\n self.add(f\"animation.register_event_handler(\\\"{event_name}\\\", {handler_name}, 0, nil, {event_params})\")\n end\n \n # Process event parameters: timer(5s) -> {\"interval\": 5000}\n def process_event_parameters()\n self.expect_left_paren()\n var params = \"{\"\n \n # For timer events, convert time to milliseconds\n if !self.at_end() && !self.check_right_paren()\n var tok = self.current()\n if tok != nil && tok.type == 5 #-animation_dsl.Token.TIME-#\n var time_ms = self.process_time_value()\n params += f\"\\\"interval\\\": {time_ms}\"\n else\n var value_result = self.process_value(\"event_param\")\n params += f\"\\\"value\\\": {value_result.expr}\"\n end\n end\n \n self.expect_right_paren()\n params += \"}\"\n return params\n end\n \n # Process berry code block: berry \"\"\"\"\"\" or berry ''''''\n def process_berry_code_block()\n self.next() # skip 'berry'\n \n # Expect a string token containing the berry code\n var tok = self.current()\n if tok == nil || tok.type != 3 #-animation_dsl.Token.STRING-#\n self.error(\"Expected string literal after 'berry' keyword. Use berry \\\"\\\"\\\"\\\"\\\"\\\" or berry ''''''\")\n self.skip_statement()\n return\n end\n \n var berry_code = tok.value\n self.next() # consume string token\n \n var inline_comment = self.collect_inline_comment()\n \n # Add the berry code verbatim to the output\n self.add(f\"# Berry code block{inline_comment}\")\n \n # Split the berry code into lines and add each line\n import string\n var lines = string.split(berry_code, '\\n')\n for line : lines\n self.add(line)\n end\n \n self.add(\"# End berry code block\")\n end\n\n # Process external function declaration: extern function function_name\n def process_external_function()\n self.next() # skip 'extern'\n \n # Expect 'function' keyword\n var tok = self.current()\n if tok == nil || tok.type != 0 #-animation_dsl.Token.KEYWORD-# || tok.value != \"function\"\n self.error(\"Expected 'function' keyword after 'extern'. Use: extern function function_name\")\n self.skip_statement()\n return\n end\n \n self.next() # skip 'function'\n \n # Expect an identifier for the function name\n tok = self.current()\n if tok == nil || tok.type != 1 #-animation_dsl.Token.IDENTIFIER-#\n self.error(\"Expected function name after 'extern function'. Use: extern function function_name\")\n self.skip_statement()\n return\n end\n \n var func_name = tok.value\n self.next() # consume identifier token\n \n var inline_comment = self.collect_inline_comment()\n \n # Check if already declared (duplicate extern function is allowed, skip code generation)\n if self.symbol_table.contains(func_name)\n var entry = self.symbol_table.get(func_name)\n if entry != nil && entry.type == 5 #-animation_dsl._symbol_entry.TYPE_USER_FUNCTION-#\n # Already declared as extern function, skip duplicate registration\n return\n end\n end\n \n # Validate function name\n self.validate_user_name(func_name, \"extern function\")\n \n # Register the function as a user function in the symbol table\n # This allows it to be used in computed parameters and function calls\n self.symbol_table.register_user_function(func_name)\n \n # Generate runtime registration call so the function is available at execution time\n self.add(f\"# External function declaration: {func_name}{inline_comment}\")\n self.add(f\"animation.register_user_function(\\\"{func_name}\\\", {func_name})\")\n end\n\n # Generate default strip initialization using Tasmota configuration\n def generate_default_strip_initialization()\n if self.strip_initialized\n return # Already initialized, don't duplicate\n end\n \n self.add(\"# Auto-generated strip initialization (using Tasmota configuration)\")\n self.add(\"var engine = animation.init_strip()\")\n self.add(\"\")\n self.strip_initialized = true\n end\n\n # Helper method to add inherited parameters from engine_proxy class hierarchy\n # This dynamically discovers all parameters from engine_proxy and its superclasses\n def _add_inherited_params_to_template(template_params_map)\n import introspect\n \n # Create a temporary engine_proxy instance to inspect its class hierarchy\n try\n var temp_engine = animation.init_strip()\n var proxy_instance = animation.engine_proxy(temp_engine)\n \n # Walk up the class hierarchy to collect all PARAMS\n var current_class = classof(proxy_instance)\n while current_class != nil\n # Check if this class has PARAMS\n if introspect.contains(current_class, \"PARAMS\")\n var class_params = current_class.PARAMS\n # Add all parameter names from this class\n for param_name : class_params.keys()\n template_params_map[param_name] = true\n end\n end\n \n # Move to parent class\n current_class = super(current_class)\n end\n except .. as e, msg\n # If we can't create the instance, fall back to a static list\n # This should include the known parameters from engine_proxy hierarchy\n var fallback_params = [\"name\", \"priority\", \"duration\", \"loop\", \"opacity\", \"color\", \"is_running\"]\n for param : fallback_params\n template_params_map[param] = true\n end\n end\n end\n \n # Generate Berry class for template animation definition\n # Creates a class extending engine_proxy with parameters as instance variables\n def generate_template_animation_class(name, params, param_types)\n import animation_dsl\n import string\n \n # Generate class definition\n self.add(f\"# Template animation class: {name}\")\n self.add(f\"class {name}_animation : animation.engine_proxy\")\n \n # Generate PARAMS static variable with encode_constraints\n self.add(\" static var PARAMS = animation.enc_params({\")\n for i : 0..size(params)-1\n var param = params[i]\n var param_constraints = param_types.find(param)\n var comma = (i < size(params) - 1) ? \",\" : \"\"\n \n if param_constraints != nil\n # param_constraints is now a map with type, min, max, default\n if type(param_constraints) == \"instance\" && classname(param_constraints) == \"map\"\n # Build constraint map string\n var constraint_parts = []\n if param_constraints.contains(\"type\")\n constraint_parts.push(f'\"type\": \"{param_constraints[\"type\"]}\"')\n end\n if param_constraints.contains(\"min\")\n constraint_parts.push(f'\"min\": {param_constraints[\"min\"]}')\n end\n if param_constraints.contains(\"max\")\n constraint_parts.push(f'\"max\": {param_constraints[\"max\"]}')\n end\n if param_constraints.contains(\"default\")\n constraint_parts.push(f'\"default\": {param_constraints[\"default\"]}')\n end\n if param_constraints.contains(\"nillable\")\n constraint_parts.push(f'\"nillable\": {param_constraints[\"nillable\"]}')\n end\n \n var constraint_str = \"\"\n for j : 0..size(constraint_parts)-1\n constraint_str += constraint_parts[j]\n if j < size(constraint_parts) - 1\n constraint_str += \", \"\n end\n end\n \n self.add(f' \"{param}\": {{{constraint_str}}}{comma}')\n else\n # Old format - just a string type\n self.add(f' \"{param}\": {{\"type\": \"{param_constraints}\"}}{comma}')\n end\n else\n self.add(f' \"{param}\": {{}}{comma}')\n end\n end\n self.add(\" })\")\n self.add(\"\")\n \n # Generate setup_template method (contains all template code)\n self.add(\" # Template setup method - overrides engine_proxy placeholder\")\n self.add(\" def setup_template()\")\n self.add(\" var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')\")\n self.add(\"\")\n \n # Create a new transpiler that shares the same pull lexer\n # It will consume tokens from the current position until the template ends\n var template_transpiler = animation_dsl.SimpleDSLTranspiler(self.pull_lexer)\n template_transpiler.symbol_table = animation_dsl._symbol_table() # Fresh symbol table for template\n template_transpiler.strip_initialized = true # Templates assume engine exists\n template_transpiler.indent_level = 2 # Start with 2 levels of indentation (inside class and setup_template method)\n \n # Set template animation parameters for special handling\n # Include both user-defined parameters AND inherited parameters from engine_proxy class hierarchy\n template_transpiler.template_animation_params = {}\n \n # Add user-defined parameters\n for param : params\n template_transpiler.template_animation_params[param] = true\n end\n \n # Add inherited parameters from engine_proxy class hierarchy dynamically\n self._add_inherited_params_to_template(template_transpiler.template_animation_params)\n \n # Add parameters to template's symbol table with proper types\n # Mark them as special \"parameter\" type so they get wrapped in closures\n for param : params\n var param_constraints = param_types.find(param)\n if param_constraints != nil\n # Extract type from constraints map (or use directly if it's a string)\n var param_type = nil\n if type(param_constraints) == \"instance\" && classname(param_constraints) == \"map\"\n param_type = param_constraints.find(\"type\")\n else\n param_type = param_constraints # Old format - just a string\n end\n \n if param_type != nil\n # Create typed parameter based on type annotation\n self._add_typed_parameter_to_symbol_table(template_transpiler.symbol_table, param, param_type)\n else\n # No type specified - default to variable\n template_transpiler.symbol_table.create_variable(param)\n end\n else\n # Default to variable type for untyped parameters\n template_transpiler.symbol_table.create_variable(param)\n end\n end\n \n # Transpile the template body - it will consume tokens until the closing brace\n var template_body = template_transpiler.transpile_template_animation_body()\n \n if template_body != nil\n # Add the transpiled body with proper indentation (4 spaces for inside setup_template method)\n var body_lines = string.split(template_body, \"\\n\")\n for line : body_lines\n if size(line) > 0\n self.add(f\" {line}\") # Add 4-space indentation for setup_template method body\n end\n end\n \n # Validate parameter usage in template body (post-transpilation check)\n self._validate_template_parameter_usage(name, params, template_body)\n else\n # Error in template body transpilation\n for error : template_transpiler.errors\n self.error(f\"Template animation '{name}' body error: {error}\")\n end\n end\n \n # Expect the closing brace (template_transpiler should have left us at this position)\n self.expect_right_brace()\n \n self.add(\" end\")\n self.add(\"end\")\n self.add(\"\")\n end\n \n # Process named arguments for animation declarations with parameter validation\n #\n # @param var_name: string - Variable name to assign parameters to\n # @param func_name: string - Animation function name for validation\n def _process_named_arguments_for_animation(var_name, func_name)\n self._process_named_arguments_unified(var_name, func_name, self.CONTEXT_ANIMATION)\n end\n \n # Create instance for parameter validation at transpile time - simplified using symbol_table\n def _create_instance_for_validation(func_name)\n # Use symbol_table's dynamic detection to get instance\n var entry = self.symbol_table.get(func_name)\n return entry != nil ? entry.instance : nil\n end\n \n # Validate a single parameter immediately as it's parsed\n #\n # @param func_name: string - Name of the animation function\n # @param param_name: string - Name of the parameter being validated\n # @param animation_instance: instance - Pre-created animation instance for validation\n def _validate_single_parameter(func_name, param_name, animation_instance)\n try\n import introspect\n \n # Validate parameter using the has_param method\n if animation_instance != nil && introspect.contains(animation_instance, \"has_param\")\n if !animation_instance.has_param(param_name)\n var line = self.current() != nil ? self.current().line : 0\n self.error(f\"Animation '{func_name}' does not have parameter '{param_name}'. Check the animation documentation for valid parameters.\")\n end\n end\n \n except \"dsl_compilation_error\" as e, msg\n # Re-raise DSL compilation errors (these are intentional validation failures)\n raise e, msg\n except .. as e, msg\n # If validation fails for any other reason, just continue\n # This ensures the transpiler is robust even if validation has issues\n end\n end\n \n # Validate that a referenced object exists in the symbol table or animation module\n #\n # @param object_name: string - Name of the object being referenced\n # @param context: string - Context where the reference occurs (for error messages)\n # @return bool: true if exists, false if not found\n def _validate_object_reference(object_name, context)\n if !self.symbol_table.symbol_exists(object_name)\n self.error(f\"Undefined reference '{object_name}' in {context}. Make sure the object is defined before use.\")\n return false\n end\n return true\n end\n\n # Validate animation factory exists - simplified using symbol_table\n def _validate_animation_factory_exists(func_name)\n # Use symbol table's dynamic detection - any callable function is valid\n var entry = self.symbol_table.get(func_name)\n return entry != nil\n end\n \n # Validate color provider factory exists - simplified using symbol_table \n def _validate_color_provider_factory_exists(func_name)\n # Use symbol table's dynamic detection - any callable function is valid\n var entry = self.symbol_table.get(func_name)\n return entry != nil && entry.type == 10 #-animation_dsl._symbol_entry.TYPE_COLOR_CONSTRUCTOR-#\n end\n \n # Validate that a referenced object is a value provider or animation - simplified using symbol_table\n def _validate_value_provider_reference(object_name, context)\n try\n # First check if symbol exists using symbol_table\n if !self.symbol_table.symbol_exists(object_name)\n self.error(f\"Undefined reference '{object_name}' in {context} statement. Make sure the value provider or animation is defined before use.\")\n return false\n end\n \n # Use symbol_table to get type information\n var entry = self.symbol_table.get(object_name)\n if entry != nil\n # Check if it's a value provider or animation instance (not constructor)\n if entry.type == 7 #-animation_dsl._symbol_entry.TYPE_VALUE_PROVIDER-# || entry.type == 9 #-animation_dsl._symbol_entry.TYPE_ANIMATION-#\n return true # Valid value provider or animation instance\n else\n # It's some other type (variable, color, sequence, constructor, etc.)\n self.error(f\"'{object_name}' in {context} statement is not a value provider or animation instance. Only value provider instances (like oscillators) and animation instances can be restarted.\")\n return false\n end\n end\n \n # For built-in symbols or sequences, assume they're valid (can't validate at compile time)\n return true\n \n except .. as e, msg\n # If validation fails for any reason, report error but continue\n self.error(f\"Could not validate '{object_name}' in {context} statement: {msg}\")\n return false\n end\n end\n \n # Core parameter processing logic that can be used by different contexts\n # @param func_name: string - Function name for validation (can be empty for variable mode)\n # @param validation_type: string - Type of validation: \"animation\", \"color_provider\", \"value_provider\", \"variable\", or \"generic\"\n # @param assignment_callback: function - Callback to handle parameter assignments, receives (param_name, param_value, inline_comment)\n def _process_parameters_core(func_name, validation_type, assignment_callback)\n # Create instance once for parameter validation based on validation type\n var instance = nil\n var effective_func_name = func_name\n \n # Create validation instance if we have a function name\n if effective_func_name != \"\"\n instance = self._create_instance_for_validation(effective_func_name)\n end\n \n while !self.at_end() && !self.check_right_paren()\n self.skip_whitespace_including_newlines()\n \n if self.check_right_paren()\n break\n end\n \n # Parse named argument: param_name=value\n var param_name = self.expect_identifier()\n \n # Validate parameter immediately as it's parsed\n if instance != nil && effective_func_name != \"\"\n self._validate_single_parameter(effective_func_name, param_name, instance)\n end\n \n self.expect_assign()\n var param_value_result = self.process_value(self.CONTEXT_VARIABLE)\n var inline_comment = self.collect_inline_comment()\n \n # Call the assignment callback to handle the parameter\n assignment_callback(param_name, param_value_result.expr, inline_comment)\n \n # Skip whitespace but preserve newlines for separator detection\n while !self.at_end()\n var tok = self.current()\n if tok != nil && tok.type == 37 #-animation_dsl.Token.COMMENT-#\n self.next()\n else\n break\n end\n end\n \n # Check for parameter separator: comma OR newline OR end of parameters\n if self.current() != nil && self.current().type == 30 #-animation_dsl.Token.COMMA-#\n self.next() # skip comma\n self.skip_whitespace_including_newlines()\n elif self.current() != nil && self.current().type == 35 #-animation_dsl.Token.NEWLINE-#\n # Newline acts as parameter separator - skip it and continue\n self.next() # skip newline\n self.skip_whitespace_including_newlines()\n elif !self.check_right_paren()\n self.error(\"Expected ',' or ')' in function arguments\")\n break\n end\n end\n end\n \n # Unified parameter processing method with validation type parameter\n # @param var_name: string - Variable name to assign parameters to\n # @param func_name: string - Function name for validation (can be empty for variable mode)\n # @param validation_type: string - Type of validation: \"animation\", \"color_provider\", \"value_provider\", \"variable\", or \"generic\"\n def _process_named_arguments_unified(var_name, func_name, validation_type)\n self.expect_left_paren()\n \n # Use the core processing logic with a callback for standard assignments\n var assignment_callback = def (param_name, param_value, inline_comment)\n self.add(f\"{var_name}.{param_name} = {param_value}{inline_comment}\")\n end\n \n self._process_parameters_core(func_name, validation_type, assignment_callback)\n self.expect_right_paren()\n end\n \n def _process_named_arguments_for_color_provider(var_name, func_name)\n self._process_named_arguments_unified(var_name, func_name, self.CONTEXT_COLOR_PROVIDER)\n end\n\n # Template parameter validation methods\n \n # Validate template parameter name\n def _validate_template_parameter_name(param_name, param_names_seen, is_template_animation)\n import animation_dsl\n # Check for duplicate parameter names\n if param_names_seen.contains(param_name)\n self.error(f\"Duplicate parameter name '{param_name}' in template. Each parameter must have a unique name.\")\n return false\n end\n \n # Check if parameter name conflicts with reserved keywords\n var reserved_keywords = [\n \"engine\", \"self\", \"animation\", \"color\", \"palette\", \"sequence\", \"template\",\n \"import\", \"def\", \"end\", \"class\", \"var\", \"if\", \"else\", \"while\", \"for\",\n \"true\", \"false\", \"nil\", \"return\", \"break\", \"continue\"\n ]\n \n for keyword : reserved_keywords\n if param_name == keyword\n self.error(f\"Parameter name '{param_name}' conflicts with reserved keyword. Use a different name like '{param_name}_param' or 'my_{param_name}'.\")\n return false\n end\n end\n \n # Check if parameter name conflicts with built-in color names\n if animation_dsl.is_color_name(param_name)\n self.error(f\"Parameter name '{param_name}' conflicts with built-in color name. Use a different name like '{param_name}_param' or 'my_{param_name}'.\")\n return false\n end\n \n # For template animations, check if parameter masks an existing parameter from engine_proxy or Animation\n if is_template_animation\n var base_class_params = [\n \"name\", \"is_running\", \"priority\", \"duration\", \"loop\", \"opacity\", \"color\"\n ]\n \n for base_param : base_class_params\n if param_name == base_param\n self.warning(f\"Template animation parameter '{param_name}' masks existing parameter from engine_proxy base class. This may cause unexpected behavior. Consider using a different name like 'custom_{param_name}' or '{param_name}_value'.\")\n break\n end\n end\n end\n \n return true\n end\n \n # Validate template parameter type annotation\n def _validate_template_parameter_type(param_type)\n var valid_types = [\n \"int\", \"bool\", \"string\", \"bytes\", \"function\", \"animation\", \n \"value_provider\", \"number\", \"color\", \"palette\", \"time\", \"percentage\", \"any\"\n ]\n \n for valid_type : valid_types\n if param_type == valid_type\n return true\n end\n end\n \n self.error(f\"Invalid parameter type '{param_type}'. Valid types are: {valid_types}\")\n return false\n end\n \n # Register template animation as an animation constructor\n # This allows it to be used like: animation x = template_name(param1=value1, ...)\n def _register_template_animation_constructor(name, params, param_types)\n import animation_dsl\n \n # Create a mock instance that has has_param method for validation\n var mock_instance = {\n \"_params\": {},\n \"has_param\": def (param_name)\n # Check if this parameter exists in the template's parameter list\n for p : params\n if p == param_name\n return true\n end\n end\n return false\n end\n }\n \n # Add all parameters to the mock instance's _params\n for param : params\n mock_instance[\"_params\"][param] = true\n end\n \n # Get the existing template entry and update it to be an animation constructor\n var existing_entry = self.symbol_table.entries.find(name)\n if existing_entry != nil\n # Update the existing entry to be an animation constructor type\n existing_entry.type = 8 # TYPE_ANIMATION_CONSTRUCTOR\n existing_entry.instance = mock_instance\n existing_entry.takes_args = true\n existing_entry.arg_type = \"named\"\n end\n end\n \n # Parse parameter constraints (type, min, max, default)\n # Returns a map with constraint keys and values, or nil if no constraints\n def _parse_parameter_constraints()\n var constraints = {}\n \n # Parse all constraint keywords until we hit a newline or end of constraints\n while !self.at_end()\n var tok = self.current()\n \n # Stop if we hit a newline or closing brace\n if tok == nil || tok.type == 35 #-animation_dsl.Token.NEWLINE-# || tok.type == 27 #-animation_dsl.Token.RIGHT_BRACE-#\n break\n end\n \n # Check for constraint keywords (can be either KEYWORD or IDENTIFIER tokens)\n if tok.type == 0 #-animation_dsl.Token.KEYWORD-# || tok.type == 1 #-animation_dsl.Token.IDENTIFIER-#\n if tok.value == \"type\"\n self.next() # skip 'type'\n var param_type = self.expect_identifier()\n \n # Validate type annotation\n if !self._validate_template_parameter_type(param_type)\n return nil\n end\n \n constraints[\"type\"] = param_type\n \n elif tok.value == \"min\"\n self.next() # skip 'min'\n # Use process_value to handle all value types (numbers, time, colors, etc.)\n var min_result = self.process_value(self.CONTEXT_GENERIC)\n if min_result != nil && min_result.expr != nil\n # Try to evaluate the expression to get a concrete value\n # For simple literals, the expr will be the value itself\n constraints[\"min\"] = min_result.expr\n else\n self.error(\"Expected value after 'min'\")\n return nil\n end\n \n elif tok.value == \"max\"\n self.next() # skip 'max'\n # Use process_value to handle all value types (numbers, time, colors, etc.)\n var max_result = self.process_value(self.CONTEXT_GENERIC)\n if max_result != nil && max_result.expr != nil\n # Try to evaluate the expression to get a concrete value\n # For simple literals, the expr will be the value itself\n constraints[\"max\"] = max_result.expr\n else\n self.error(\"Expected value after 'max'\")\n return nil\n end\n \n elif tok.value == \"default\"\n self.next() # skip 'default'\n # Use process_value to handle all value types (numbers, time, colors, etc.)\n var default_result = self.process_value(self.CONTEXT_GENERIC)\n if default_result != nil && default_result.expr != nil\n # Store the expression as the default value\n constraints[\"default\"] = default_result.expr\n else\n self.error(\"Expected value after 'default'\")\n return nil\n end\n \n elif tok.value == \"nillable\"\n self.next() # skip 'nillable'\n var nillable_tok = self.current()\n if nillable_tok != nil && nillable_tok.type == 0 #-animation_dsl.Token.KEYWORD-#\n if nillable_tok.value == \"true\"\n self.next()\n constraints[\"nillable\"] = true\n elif nillable_tok.value == \"false\"\n self.next()\n constraints[\"nillable\"] = false\n else\n self.error(\"Expected 'true' or 'false' after 'nillable'\")\n return nil\n end\n else\n self.error(\"Expected 'true' or 'false' after 'nillable'\")\n return nil\n end\n \n else\n # Unknown keyword - stop parsing constraints\n break\n end\n else\n # Not a keyword or identifier - stop parsing constraints\n break\n end\n end\n \n return size(constraints) > 0 ? constraints : nil\n end\n \n # Add typed parameter to symbol table based on type annotation\n def _add_typed_parameter_to_symbol_table(symbol_table, param_name, param_type)\n if param_type == \"color\"\n symbol_table.create_color(param_name, nil)\n elif param_type == \"palette\"\n symbol_table.create_palette(param_name, nil)\n elif param_type == \"animation\"\n symbol_table.create_animation(param_name, nil)\n elif param_type == \"value_provider\"\n symbol_table.create_value_provider(param_name, nil)\n else\n # Default to variable for number, string, bool, time, percentage, function\n symbol_table.create_variable(param_name)\n end\n end\n \n # Validate template parameter usage in generated body\n def _validate_template_parameter_usage(template_name, params, template_body)\n import string\n \n # Check if each parameter is actually used in the template body\n for param : params\n # Check for both regular template usage (param_) and template animation usage (self.param)\n var param_ref_regular = f\"{param}_\"\n var param_ref_animation = f\"self.{param}\"\n \n if string.find(template_body, param_ref_regular) == -1 && string.find(template_body, param_ref_animation) == -1\n # Parameter not found in body - this is a warning, not an error\n self.warning(f\"Template '{template_name}' parameter '{param}' is declared but never used in the template body.\")\n end\n end\n end\n \n # Validate template call arguments (called when processing template calls)\n def _validate_template_call_arguments(template_name, provided_args, expected_params, param_types)\n # Check argument count\n if size(provided_args) != size(expected_params)\n self.error(f\"Template '{template_name}' expects {size(expected_params)} arguments but {size(provided_args)} were provided. Expected parameters: {expected_params}\")\n return false\n end\n \n # TODO: Add type checking for arguments based on param_types\n # This would require more sophisticated type inference for the provided arguments\n \n return true\n end\n \n # Helper method to split function arguments string into array\n def _split_function_arguments(args_str)\n import string\n \n if args_str == \"\" || args_str == nil\n return []\n end\n \n # Simple split by comma - this is a basic implementation\n # A more sophisticated version would handle nested parentheses and quotes\n var args = string.split(args_str, \",\")\n var result = []\n \n for arg : args\n # Trim whitespace\n var trimmed = string.strip(arg)\n if size(trimmed) > 0\n result.push(trimmed)\n end\n end\n \n return result\n end\n\nend\n\n# DSL compilation function\ndef compile_dsl(source)\n import animation_dsl\n var lexer = animation_dsl.create_lexer(source)\n var transpiler = animation_dsl.SimpleDSLTranspiler(lexer)\n var berry_code = transpiler.transpile()\n \n return berry_code\nend\n\n# Return module exports\nreturn {\n \"SimpleDSLTranspiler\": SimpleDSLTranspiler,\n \"compile_dsl\": compile_dsl,\n}"; modules["providers/breathe_color_provider.be"] = "# Breathe Color Provider for Berry Animation Framework\n#\n# This color provider creates breathing/pulsing color effects by modulating the brightness\n# of a base color over time. It inherits from color_provider to leverage its color handling\n# and maintains an internal oscillator_value for time-based brightness modulation.\n#\n# The effect uses the oscillator's COSINE waveform with optional curve factor:\n# - curve_factor 1: Pure cosine wave (smooth pulsing)\n# - curve_factor 2-5: Natural breathing with pauses at peaks (5 = most pronounced pauses)\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass breathe_color : animation.color_provider\n # Internal oscillator for brightness modulation\n var _oscillator\n \n # Parameter definitions for breathing-specific functionality\n # The 'color' and 'brightness' parameters are inherited from color_provider\n static var PARAMS = animation.enc_params({\n \"min_brightness\": {\"min\": 0, \"max\": 255, \"default\": 0}, # Minimum brightness level (0-255)\n \"max_brightness\": {\"min\": 0, \"max\": 255, \"default\": 255}, # Maximum brightness level (0-255)\n \"period\": {\"min\": 1, \"default\": 3000}, # Breathing cycle time in ms (renamed from duration for consistency)\n \"curve_factor\": {\"min\": 1, \"max\": 5, \"default\": 2} # Factor to control breathing curve shape (1=cosine wave, 2-5=curved breathing with pauses)\n })\n \n # Initialize a new Breathe Color Provider\n # Following parameterized class specification - engine parameter only\n #\n # @param engine: AnimationEngine - The animation engine (required)\n def init(engine)\n # Call parent constructor (color_provider)\n super(self).init(engine)\n \n # Create internal oscillator for brightness modulation\n self._oscillator = animation.oscillator_value(engine)\n self._oscillator.form = 4 #-animation.COSINE-# # Use cosine wave for smooth breathing\n self._oscillator.min_value = 0 # Fixed range 0-255 for normalized oscillation\n self._oscillator.max_value = 255 # Fixed range 0-255 for normalized oscillation\n self._oscillator.duration = 3000 # Default period (synced with our 'period' param)\n engine.add(self._oscillator) # register so it receives start()\n end\n \n # Handle parameter changes - sync period to internal oscillator\n def on_param_changed(name, value)\n # Sync period changes to the internal oscillator's duration\n if name == \"period\"\n self._oscillator.duration = value\n end\n \n # Call parent's parameter change handler\n super(self).on_param_changed(name, value)\n end\n \n # Produce color value based on current time\n # This overrides the parent's produce_value to return colors with modulated brightness\n #\n # @param name: string - Parameter name (ignored for color providers)\n # @param time_ms: int - Current time in milliseconds\n # @return int - 32-bit ARGB color value with modulated brightness\n def produce_value(name, time_ms)\n # Get the normalized oscillator value (0-255) from internal oscillator\n var normalized_value = self._oscillator.produce_value(name, time_ms)\n \n # Apply curve factor if > 1 for natural breathing effect\n var current_curve_factor = self.curve_factor\n var curved_value = normalized_value\n \n if current_curve_factor > 1\n # Apply curve factor to the normalized value\n # Convert to 0-8192 range for curve calculation (fixed point math)\n var curve_input = tasmota.scale_uint(normalized_value, 0, 255, 0, 8192)\n \n # Apply power function to create curve\n var factor = current_curve_factor\n while factor > 1\n curve_input = (curve_input * curve_input) / 8192\n factor -= 1\n end\n \n # Convert back to 0-255 range\n curved_value = tasmota.scale_uint(curve_input, 0, 8192, 0, 255)\n end\n \n # Now map the curved value to the brightness range\n var brightness = tasmota.scale_uint(curved_value, 0, 255, self.min_brightness, self.max_brightness)\n \n # Get the base color (inherited from color_provider)\n var current_color = self.color\n\n # Apply brightness\n return self.apply_brightness(current_color, brightness)\n end\nend\n\nreturn {'breathe_color': breathe_color}"; modules["providers/closure_value_provider.be"] = "# closure_value - value_provider that wraps a closure/function\n#\n# This provider allows using closures (functions) as value providers.\n# The closure is called with (self, param_name, time_ms) parameters when\n# a value is requested.\n#\n# Usage:\n# var provider = animation.closure_value_provider(engine)\n# provider.closure = def(self, param_name, time_ms) return time_ms / 100 end\n# animation.brightness = provider\n# Alternative with reference to another value:\n# var strip_len_ = animation.strip_length(engine)\n# var provider = animation.closure_value_provider(engine)\n# provider.closure = def(self, param_name, time_ms) return self.resolve(strip_len_, param_name, timer_ms) + 2 end\n# animation.brightness = provider\n# \n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass closure_value : animation.parameterized_object\n static var VALUE_PROVIDER = true\n var _closure # We keep the closure as instance variable for faster dereferencing, in addition to PARAMS\n\n # Static parameter definitions\n static var PARAMS = animation.enc_params({\n \"closure\": {\"type\": \"function\", \"default\": nil}\n })\n \n # Method called when a parameter is changed\n # Copy \"closure\" parameter to _closure instance variable\n #\n # @param name: string - Parameter name\n # @param value: any - New parameter value\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"closure\"\n self._closure = value\n end\n end\n \n # Produce a value by calling the stored closure\n #\n # @param name: string - Parameter name being requested\n # @param time_ms: int - Current time in milliseconds\n # @return any - Value returned by the closure\n def produce_value(name, time_ms)\n var closure = self._closure\n if closure == nil\n return nil\n end\n\n # Call the closure with the parameter self, name and time\n return closure(self.engine, name, time_ms)\n end\nend\n\n# Create a closure_value in a single call, by passing the closure argument\n#\n# This is used only by the transpiler, and is not usable in the DSL by itself\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @param closure: function - the closure to evaluate at run-time\n# @return closure_value - New closure_value instance\ndef create_closure_value(engine, closure)\n var provider = animation.closure_value(engine)\n provider.closure = closure\n return provider\nend\n\n# Helper method to resolve a value that can be either static or from a value provider\n# This is equivalent to 'resolve_param' but with a shorter name\n# and available in animation module\n#\n# @param value: any - Static value, value provider instance, or parameterized object\n# @param param_name: string - Parameter name for specific produce_value() method lookup\n# @return any - The resolved value (static, from provider, or from object parameter)\ndef animation_resolve(value, param_name, time_ms)\n if animation.is_value_provider(value)\n return value.produce_value(param_name, time_ms)\n elif value != nil && isinstance(value, animation.parameterized_object)\n # Handle parameterized objects (animations, etc.) by accessing their parameters\n # Check that param_name is not nil to prevent runtime errors\n if param_name == nil\n raise \"value_error\", \"Parameter name cannot be nil when resolving object parameter\"\n end\n return value.get_param_value(param_name)\n else\n return value\n end\nend\n\nreturn {'closure_value': closure_value,\n 'create_closure_value': create_closure_value,\n 'resolve': animation_resolve}"; modules["providers/color_cycle_color_provider.be"] = "# color_cycle for Berry Animation Framework\n#\n# This color provider cycles through a list of colors with brutal switching.\n# No transitions or interpolation - just instant color changes.\n#\n# Modes:\n# - Auto-cycle: period > 0 - colors change automatically at regular intervals\n# - Manual-only: period = 0 - colors only change when 'next' parameter is set to 1\n#\n# Follows the parameterized class specification:\n# - Constructor takes only 'engine' parameter\n# - All other parameters set via virtual member assignment after creation\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass color_cycle : animation.color_provider\n # Non-parameter instance variables only\n var current_index # Current color index for next functionality\n \n # Parameter definitions\n static var PARAMS = animation.enc_params({\n \"colors\": {\"type\": \"bytes\", \"default\":nil},\n \"period\": {\"min\": 0, \"default\": 5000}, # 0 = manual only, >0 = auto cycle time in ms\n \"next\": {\"default\": 0}, # Write `` to move to next colors\n \"palette_size\": {\"type\": \"int\", \"default\": 3} # Read-only: number of colors in palette\n })\n \n # Initialize a new color_cycle\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n super(self).init(engine) # Initialize parameter system\n \n # Initialize non-parameter instance variables\n self.current_index = 0 # Start at first color\n \n # Initialize palette_size parameter\n self.values[\"palette_size\"] = self._get_palette_size()\n end\n \n # Get color at a specific index from bytes palette\n # We force alpha channel to 0xFF to force opaque colors\n def _get_color_at_index(idx)\n var palette_bytes = self.colors\n var palette_size = size(palette_bytes) / 4 # Each color is 4 bytes (AARRGGBB)\n \n if (palette_size == 0) || (idx >= palette_size) || (idx < 0)\n return 0x00000000 # Default to transparent\n end\n \n # Read 4 bytes in big-endian format (AARRGGBB)\n var color = palette_bytes.get(idx * 4, -4) # Big endian\n color = color | 0xFF000000 # force full opacity\n return color\n end\n \n # Get the number of colors in the palette\n def _get_palette_size()\n return size(self.colors) / 4 # Each color is 4 bytes\n end\n \n # Virtual member access - implements the virtual \"palette_size\" attribute\n #\n # @param name: string - Parameter name being accessed\n # @return any - Resolved parameter value (value_provider resolved to actual value)\n def member(name)\n if name == \"palette_size\"\n return self._get_palette_size()\n else\n var val = super(self).member(name)\n # If 'palette' is 'nil', default to 'animation.PALETTE_RAINBOW'\n if name == \"colors\" && val == nil\n val = animation.PALETTE_RAINBOW\n end\n return val\n end\n end\n\n # Adjust index according to palette_size\n #\n # @param palette_size: int - Size of palette in colors, passed as parameter to avoid recalculating it\n def _adjust_index()\n var palette_size = self._get_palette_size()\n if palette_size > 0\n # Apply modulo palette size\n var index = self.current_index % palette_size\n # It is still possible to be negative\n if index < 0\n index += palette_size\n end\n # If index changed, invalidate color\n if self.current_index != index\n self.current_index = index\n end\n\n else\n self.current_index = 0 # default value when empty palette\n end\n end\n\n # Handle parameter changes\n #\n # @param name: string - Name of the parameter that changed\n # @param value: any - New value of the parameter\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"palette_size\"\n # palette_size is read-only - restore the actual value and raise an exception\n self.values[\"palette_size\"] = self._get_palette_size()\n raise \"value_error\", \"Parameter 'palette_size' is read-only\"\n\n elif name == \"next\" && value != 0\n # Add to color index\n self.current_index += value\n self._adjust_index()\n\n # Reset the next parameter back to 0\n self.values[\"next\"] = 0\n end\n end\n \n # Produce a color value for any parameter name\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds\n # @return int - Color in ARGB format (0xAARRGGBB)\n def produce_value(name, time_ms)\n # Get parameter values using virtual member access\n var period = self.period\n \n # Get the number of colors in the palette\n var palette_size = self._get_palette_size()\n\n if (palette_size <= 1) || (period == 0) # no cycling stop here\n var idx = self.current_index\n if (idx >= palette_size) idx = palette_size - 1 end\n if (idx < 0) idx = 0 end\n self.current_index = idx\n var color = self._get_color_at_index(self.current_index)\n \n # Apply brightness scaling\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\n \n # Auto-cycle mode: calculate which color to show based on time (brutal switching using integer math)\n var time_in_cycle = time_ms % period\n var color_index = tasmota.scale_uint(time_in_cycle, 0, period - 1, 0, palette_size - 1)\n \n # Clamp to valid range (safety check)\n if color_index >= palette_size\n color_index = palette_size - 1\n end\n \n # Update current state and get the color\n self.current_index = color_index\n var color = self._get_color_at_index(color_index)\n \n # Apply brightness scaling\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\n \n # Get a color based on a value (maps value to position in cycle)\n # This method is kept for backward compatibility - brutal switching based on value\n #\n # @param value: int/float - Value to map to a color (0-255 range)\n # @param time_ms: int - Current time in milliseconds (ignored for value-based color)\n # @return int - Color in ARGB format (0xAARRGGBB)\n def get_color_for_value(value, time_ms)\n # Get the number of colors in the palette\n var palette_size = self._get_palette_size()\n if palette_size == 0\n return 0x00000000 # Default to transparent if no colors\n end\n \n if palette_size == 1\n var color = self._get_color_at_index(0) # If only one color, just return it\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\n \n # Clamp value to 0-255\n if value < 0\n value = 0\n elif value > 255\n value = 255\n end\n \n # Map value directly to color index (brutal switching using integer math)\n var color_index = tasmota.scale_uint(value, 0, 255, 0, palette_size - 1)\n \n # Clamp to valid range\n if color_index >= palette_size\n color_index = palette_size - 1\n end\n \n var color = self._get_color_at_index(color_index)\n \n # Apply brightness scaling\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\nend\n\nreturn {'color_cycle': color_cycle}\n"; modules["providers/color_provider.be"] = "# color_provider interface for Berry Animation Framework\n#\n# This defines the core interface for color providers in the animation framework.\n# Color providers generate colors based on time or values, which can be used by\n# renderers or other components that need color information.\n#\n# color_provider now inherits from value_provider, making it a specialized value provider\n# for color values. This provides consistency with the value_provider system while\n# maintaining the specific color-related methods.\n#\n# Follows the parameterized class specification:\n# - Constructor takes only 'engine' parameter\n# - All other parameters set via virtual member assignment after creation\n\nclass color_provider : animation.parameterized_object\n static var VALUE_PROVIDER = true\n # LUT (Lookup Table) management for color providers\n # Subclasses can use this to cache pre-computed colors for performance\n # If a subclass doesn't use a LUT, this remains nil\n var _color_lut # Color lookup table cache (bytes() object or nil)\n var _lut_dirty # Flag indicating LUT needs rebuilding\n static var LUT_FACTOR = 1 # Reduction factor for LUT compression\n \n # Parameter definitions\n static var PARAMS = animation.enc_params({\n \"color\": {\"default\": 0xFFFFFFFF}, # Default to white\n \"brightness\": {\"min\": 0, \"max\": 255, \"default\": 255}\n })\n \n # Initialize the color provider\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n super(self).init(engine)\n self._color_lut = nil\n self._lut_dirty = true\n end\n \n # Get the color lookup table\n # Returns the LUT bytes() object if the provider uses one, or nil otherwise\n #\n # @return bytes|nil - The LUT bytes object or nil\n def get_lut()\n return self._color_lut\n end\n \n # Produce a color value for any parameter name\n # Returns the solid color with brightness applied\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds (ignored)\n # @return int - Color in ARGB format (0xAARRGGBB)\n def produce_value(name, time_ms)\n var color = self.color\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\n \n # Get the solid color for a value (ignores the value)\n #\n # @param value: int/float - Value to map to a color (ignored)\n # @param time_ms: int - Current time in milliseconds (ignored)\n # @return int - Color in ARGB format (0xAARRGGBB)\n def get_color_for_value(value, time_ms)\n return self.produce_value(\"color\", time_ms) # Default: use time-based color\n end\n \n # Static method to apply brightness scaling to a color\n # Only performs scaling if brightness is not 255 (full brightness)\n #\n # @param color: int - Color in ARGB format (0xAARRGGBB)\n # @param brightness: int - Brightness level (0-255)\n # @return int - Color with brightness applied\n static def apply_brightness(color, brightness)\n # Skip scaling if brightness is full (255)\n if brightness == 255\n return color\n end\n \n # Extract RGB components (preserve alpha channel)\n var alpha = (color >> 24) & 0xFF\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n # Scale each component by brightness\n r = tasmota.scale_uint(r, 0, 255, 0, brightness)\n g = tasmota.scale_uint(g, 0, 255, 0, brightness)\n b = tasmota.scale_uint(b, 0, 255, 0, brightness)\n \n # Reconstruct color with scaled brightness (preserve alpha)\n return (alpha << 24) | (r << 16) | (g << 8) | b\n end\n\nend\n\n# Add a method to check if an object is a color provider\n# Note: Since color_provider now inherits from value_provider, all ColorProviders\n# are also value_providers and will be detected by animation.is_value_provider()\ndef is_color_provider(obj)\n return isinstance(obj, animation.color_provider)\nend\n\nreturn {'color_provider': color_provider,\n 'is_color_provider': is_color_provider}"; modules["providers/iteration_number_provider.be"] = "# iteration_number - value_provider that returns current sequence iteration number\n#\n# This provider returns the current iteration number (0-based) for the innermost\n# sequence context, or nil if not called within a sequence.\n#\n# The iteration number is tracked by the animation engine using a stack-based\n# approach to handle nested sequences properly.\n#\n# Usage:\n# set iteration = iteration_number()\n# animation pulse = breathe(color=red, period=2s)\n# pulse.opacity = iteration * 50 + 100 # Brightness increases with each iteration\n#\n# In sequences:\n# sequence demo {\n# repeat 5 times {\n# play pulse for 1s\n# # iteration will be 0, 1, 2, 3, 4 for each repeat\n# }\n# }\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass iteration_number : animation.parameterized_object\n static var VALUE_PROVIDER = true\n # Produce the current iteration number from the animation engine\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds (ignored)\n # @return int|nil - Current iteration number (0-based) or nil if not in sequence\n def produce_value(name, time_ms)\n # Get the current iteration number from the engine's sequence stack\n return self.engine.get_current_iteration_number()\n end\nend\n\nreturn {'iteration_number': iteration_number}"; modules["providers/oscillator_value_provider.be"] = "# oscillator_value for Berry Animation Framework\n#\n# Generates oscillating values based on time using various waveforms.\n#\n# Supported waveforms:\n# - SAWTOOTH (1): Linear ramp from min to max, then reset\n# - TRIANGLE (2): Linear ramp from min to max, then back to min\n# - SQUARE (3): Alternates between min and max (duty_cycle controls ratio)\n# - COSINE (4): Smooth cosine wave (starts at min, peaks at mid-cycle)\n# - SINE (5): Pure sine wave (starts at mid-value)\n# - EASE_IN (6): Quadratic acceleration (slow start, fast end)\n# - EASE_OUT (7): Quadratic deceleration (fast start, slow end)\n# - ELASTIC (8): Spring-like overshoot and oscillation\n# - BOUNCE (9): Ball-like bouncing with decreasing amplitude\n\nimport \"./core/param_encoder\" as encode_constraints\n\n# Waveform constants\nvar SAWTOOTH = 1\nvar LINEAR = 1\nvar TRIANGLE = 2\nvar SQUARE = 3\nvar COSINE = 4\nvar SINE = 5\nvar EASE_IN = 6\nvar EASE_OUT = 7\nvar ELASTIC = 8\nvar BOUNCE = 9\n\nclass oscillator_value : animation.parameterized_object\n static var VALUE_PROVIDER = true\n # Non-parameter instance variables only\n var value # current calculated value\n \n # Parameter definitions for the oscillator\n static var PARAMS = animation.enc_params({\n \"min_value\": {\"default\": 0},\n \"max_value\": {\"default\": 255},\n \"duration\": {\"min\": 1, \"default\": 1000},\n \"form\": {\"enum\": [1, 2, 3, 4, 5, 6, 7, 8, 9], \"default\": 1},\n \"phase\": {\"min\": 0, \"max\": 255, \"default\": 0},\n \"duty_cycle\": {\"min\": 0, \"max\": 255, \"default\": 127}\n })\n \n # Initialize a new oscillator_value\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n super(self).init(engine) # Initialize parameter system\n \n # Initialize non-parameter instance variables\n self.value = 0 # Will be calculated on first produce_value call\n end\n \n # Start/restart the oscillator at a specific time\n #\n # start() is typically not called at beginning of animations for value providers.\n # The start_time is set at the first call to produce_value().\n # This method is mainly aimed at restarting the value provider start_time\n # via the `restart` keyword in DSL.\n #\n # @param time_ms: int - Time in milliseconds to set as start_time (optional, uses engine time if nil)\n # @return self for method chaining\n def start(time_ms)\n super(self).start(time_ms)\n return self\n end\n\n # Produce oscillator value for any parameter name\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds\n # @return number - Calculated oscillator value\n def produce_value(name, time_ms)\n # Get parameter values using virtual member access\n var member = self.member\n var duration = member(self, \"duration\")\n var min_value = member(self, \"min_value\")\n var max_value = member(self, \"max_value\")\n var form = member(self, \"form\")\n var phase = member(self, \"phase\")\n var duty_cycle = member(self, \"duty_cycle\")\n var scale_uint = tasmota.scale_uint\n var scale_int = tasmota.scale_int\n \n # Ensure time_ms is valid and initialize start_time if needed\n time_ms = self._fix_time_ms(time_ms)\n\n if duration == nil || duration <= 0\n return min_value\n end\n\n # Calculate elapsed time with cycle wrapping\n var past = time_ms - self.start_time\n if past < 0\n past = 0\n end\n if past >= duration\n self.start_time += (past / duration) * duration\n past = past % duration\n end\n \n # Apply phase shift\n if phase > 0\n past += scale_uint(phase, 0, 255, 0, duration)\n if past >= duration\n past -= duration\n end\n end\n \n # Compute value directly in min_value..max_value range\n var v\n var duty_mid = scale_uint(duty_cycle, 0, 255, 0, duration)\n \n if form == 3 #-SQUARE-#\n v = past < duty_mid ? min_value : max_value\n elif form == 2 #-TRIANGLE-#\n if past < duty_mid\n v = scale_uint(past, 0, duty_mid - 1, min_value, max_value)\n else\n v = scale_uint(past, duty_mid, duration - 1, max_value, min_value)\n end\n elif form == 4 || form == 5 #-COSINE/SINE-#\n var angle = scale_uint(past, 0, duration - 1, 0, 32767)\n if form == 4 angle -= 8192 end # cosine phase shift\n v = scale_int(tasmota.sine_int(angle), -4096, 4096, min_value, max_value)\n elif form == 6 || form == 7 #-EASE_IN/EASE_OUT-#\n var t = scale_uint(past, 0, duration - 1, 0, 255)\n if form == 6 # ease_in: t^2\n v = scale_int(t * t, 0, 65025, min_value, max_value)\n else # ease_out: 1-(1-t)^2\n var inv = 255 - t\n v = scale_int(65025 - inv * inv, 0, 65025, min_value, max_value)\n end\n elif form == 8 #-ELASTIC-#\n var t = scale_uint(past, 0, duration - 1, 0, 255)\n if t == 0 return (self.value := min_value) end\n if t == 255 return (self.value := max_value) end\n var decay = scale_uint(255 - t, 0, 255, 255, 32)\n var osc = tasmota.sine_int(scale_uint(t, 0, 255, 0, 196602) % 32767)\n var val_range = max_value - min_value\n var offset = scale_int(osc * decay, -1044480, 1044480, -val_range, val_range)\n v = min_value + scale_int(t, 0, 255, 0, val_range) + offset\n # Clamp with 25% overshoot allowance\n var overshoot = val_range / 4\n if v > max_value + overshoot v = max_value + overshoot end\n if v < min_value - overshoot v = min_value - overshoot end\n elif form == 9 #-BOUNCE-#\n var t = scale_uint(past, 0, duration - 1, 0, 255)\n var val_range = max_value - min_value\n if t < 128\n var s = scale_uint(t, 0, 127, 0, 255)\n v = max_value - scale_int((255-s)*(255-s), 0, 65025, 0, val_range)\n elif t < 192\n var s = scale_uint(t - 128, 0, 63, 0, 255)\n v = max_value - scale_int((255-s)*(255-s), 0, 65025, 0, val_range / 2)\n else\n var s = scale_uint(t - 192, 0, 63, 0, 255)\n v = max_value - scale_int((255-s)*(255-s), 0, 65025, 0, val_range / 4)\n end\n else #-SAWTOOTH (default)-#\n v = scale_uint(past, 0, duration - 1, min_value, max_value)\n end\n \n return (self.value := v)\n end\nend\n\n# Static constructor functions for common use cases\n\n# Note: The 'oscillator' function has been removed since the easing keyword is now 'ramp'\n# Use ramp() function instead for the same functionality\n\n# Create a ramp (same as oscillator, for semantic clarity)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New ramp instance\ndef ramp(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 1 #-animation.SAWTOOTH-#\n return osc\nend\n\n# Create a linear oscillator (triangle wave)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New linear oscillator instance\ndef linear(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 2 #-animation.TRIANGLE-#\n return osc\nend\n\n# Create a smooth oscillator (cosine wave)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New smooth oscillator instance\ndef smooth(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 4 #-animation.COSINE-#\n return osc\nend\n\n# Create a cosine oscillator (alias for smooth - cosine wave)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New cosine oscillator instance\ndef cosine_osc(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 4 #-animation.COSINE-#\n return osc\nend\n\n# Create a sine wave oscillator\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New sine wave instance\ndef sine_osc(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 5 #-animation.SINE-#\n return osc\nend\n\n# Create a square wave oscillator\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New square wave instance\ndef square(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 3 #-animation.SQUARE-#\n return osc\nend\n\n# Create an ease-in oscillator (quadratic acceleration)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New ease-in instance\ndef ease_in(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 6 #-animation.EASE_IN-#\n return osc\nend\n\n# Create an ease-out oscillator (quadratic deceleration)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New ease-out instance\ndef ease_out(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 7 #-animation.EASE_OUT-#\n return osc\nend\n\n# Create an elastic oscillator (spring-like overshoot and oscillation)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New elastic instance\ndef elastic(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 8 #-animation.ELASTIC-#\n return osc\nend\n\n# Create a bounce oscillator (ball-like bouncing with decreasing amplitude)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New bounce instance\ndef bounce(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 9 #-animation.BOUNCE-#\n return osc\nend\n\n# Create a sawtooth oscillator (alias for ramp - linear progression from min_value to max_value)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New sawtooth instance\ndef sawtooth(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 1 #-animation.SAWTOOTH-#\n return osc\nend\n\n# Create a triangle oscillator (alias for linear - triangle wave from min_value to max_value and back)\n#\n# @param engine: AnimationEngine - Animation engine reference\n# @return oscillator_value - New triangle instance\ndef triangle(engine)\n var osc = animation.oscillator_value(engine)\n osc.form = 2 #-animation.TRIANGLE-#\n return osc\nend\n\nreturn {'ramp': ramp,\n 'sawtooth': sawtooth,\n 'linear': linear,\n 'triangle': triangle,\n 'smooth': smooth,\n 'cosine_osc': cosine_osc,\n 'sine_osc': sine_osc,\n 'square': square,\n 'ease_in': ease_in,\n 'ease_out': ease_out,\n 'elastic': elastic,\n 'bounce': bounce,\n 'SAWTOOTH': SAWTOOTH,\n 'LINEAR': LINEAR,\n 'TRIANGLE': TRIANGLE,\n 'SQUARE': SQUARE,\n 'COSINE': COSINE,\n 'SINE': SINE,\n 'EASE_IN': EASE_IN,\n 'EASE_OUT': EASE_OUT,\n 'ELASTIC': ELASTIC,\n 'BOUNCE': BOUNCE,\n 'oscillator_value': oscillator_value}"; modules["providers/rich_palette_color_provider.be"] = "# rich_palette_color for Berry Animation Framework\n#\n# This color provider generates colors from a palette with smooth transitions.\n# Reuses optimizations from Animate_palette class for maximum efficiency.\n#\n# PERFORMANCE OPTIMIZATION - LUT Cache:\n# =====================================\n# To avoid expensive palette interpolation on every pixel (binary search + RGB interpolation\n# + brightness calculations), this provider uses a Lookup Table (LUT) cache:\n#\n# - LUT Structure: 129 entries covering values 0, 2, 4, 6, ..., 254, 255\n# - Memory Usage: 516 bytes (129 entries \u00d7 4 bytes per ARGB color)\n# - Resolution: 2-step resolution (ignoring LSB) plus special case for value 255\n# - Mapping: lut_index = value >> 1 (divide by 2), except value 255 -> index 128\n#\n# Performance Impact:\n# - Before: ~50-100 CPU cycles per lookup (search + interpolate + brightness)\n# - After: ~10-15 CPU cycles per lookup (bit shift + bytes.get())\n# - Speedup: ~5-10x faster per lookup\n# - For 60-pixel gradient at 30 FPS: ~200x reduction in expensive operations\n#\n# LUT Invalidation:\n# - Automatically rebuilt when palette, brightness, or transition_type changes\n# - Lazy initialization: built on first use of get_color_for_value()\n# - Transparent to users: no API changes required\n#\n# Follows the parameterized class specification:\n# - Constructor takes only 'engine' parameter\n# - All other parameters set via virtual member assignment after creation\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass rich_palette_color : animation.color_provider\n # Non-parameter instance variables only\n var _slots_arr # Constructed array of timestamp slots, based on period\n var _value_arr # Constructed array of value slots (always 0-255 range)\n var _slots # Number of slots in the palette\n var _brightness # Cached value for `self.brightness` used during render()\n \n # Parameter definitions\n static var PARAMS = animation.enc_params({\n \"colors\": {\"type\": \"bytes\", \"default\": nil}, # Palette bytes or predefined palette constant\n \"period\": {\"min\": 0, \"default\": 5000}, # 5 seconds default, 0 = value-based only\n \"transition_type\": {\"enum\": [1 #-animation.LINEAR-#, 5 #-animation.SINE-#], \"default\": 1 #-animation.LINEAR-#}\n # brightness parameter inherited from color_provider base class\n })\n \n # Initialize a new rich_palette_color\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n super(self).init(engine) # Initialize parameter system (also initializes LUT variables)\n \n # Initialize non-parameter instance variables\n self._slots = 0\n \n # Set default palette to animation.PALETTE_RAINBOW\n self.colors = animation.PALETTE_RAINBOW\n\n # We need to register this value provider to receive 'update()'\n engine.add(self)\n end\n \n # Handle parameter changes\n #\n # @param name: string - Name of the parameter that changed\n # @param value: any - New value of the parameter\n def on_param_changed(name, value)\n super(self).on_param_changed(name, value)\n if name == \"period\" || name == \"colors\"\n if (self._slots_arr != nil) || (self._value_arr != nil)\n # only if they were already computed\n self._recompute_palette()\n end\n end\n # Mark LUT as dirty when palette or transition_type changes\n # Note: brightness changes do NOT invalidate LUT since brightness is applied after lookup\n if name == \"colors\" || name == \"transition_type\"\n self._lut_dirty = true\n end\n # Brightness changes do NOT invalidate LUT - brightness is applied after lookup\n end\n \n # Start/restart the animation cycle at a specific time\n #\n # @param time_ms: int - Time in milliseconds to set as start_time (optional, uses engine time if nil)\n # @return self for method chaining\n def start(time_ms)\n # Compute arrays if they were not yet initialized\n if (self._slots_arr == nil) && (self._value_arr == nil)\n self._recompute_palette()\n end\n super(self).start(time_ms)\n return self\n end\n \n # Get palette bytes from parameter with default fallback\n def _get_palette_bytes()\n var palette_bytes = self.colors\n return (palette_bytes != nil) ? palette_bytes : self._DEFAULT_PALETTE\n end\n static _DEFAULT_PALETTE = bytes(\n \"00FF0000\" # Red (value 0)\n \"24FFA500\" # Orange (value 36)\n \"49FFFF00\" # Yellow (value 73)\n \"6E00FF00\" # Green (value 110)\n \"920000FF\" # Blue (value 146)\n \"B74B0082\" # Indigo (value 183)\n \"DBEE82EE\" # Violet (value 219)\n \"FFFF0000\" # Red (value 255)\n )\n \n # Recompute palette slots and metadata\n def _recompute_palette()\n # Compute slots_arr based on 'period'\n var period = self.period\n var palette_bytes = self._get_palette_bytes()\n self._slots = size(palette_bytes) / 4\n\n # Recompute palette with new cycle period (only if > 0 for time-based cycling)\n if period > 0 && palette_bytes != nil\n self._slots_arr = self._parse_palette(0, period - 1)\n else\n self._slots_arr = nil\n end\n\n # Compute value_arr for value-based mode (always 0-255 range)\n if self._get_palette_bytes() != nil\n self._value_arr = self._parse_palette(0, 255)\n else\n self._value_arr = nil\n end\n \n return self\n end\n \n # Parse the palette and create slots array (reused from Animate_palette)\n #\n # @param min: int - Minimum value for the range\n # @param max: int - Maximum value for the range\n # @return array - Array of slot positions\n def _parse_palette(min, max)\n var palette_bytes = self._get_palette_bytes()\n var arr = []\n var slots = self._slots\n arr.resize(slots)\n\n # Check if we have slots or values (exact logic from Animate_palette)\n # If first value index is non-zero, it's ticks count\n if palette_bytes.get(0, 1) != 0\n # Palette in tick counts\n # Compute the total number of ticks\n var total_ticks = 0\n var idx = 0\n while idx < slots - 1\n total_ticks += palette_bytes.get(idx * 4, 1)\n idx += 1\n end\n var cur_ticks = 0\n idx = 0\n while idx < slots\n arr[idx] = tasmota.scale_int(cur_ticks, 0, total_ticks, min, max)\n cur_ticks += palette_bytes.get(idx * 4, 1)\n idx += 1\n end\n else\n # Palette is in value range from 0..255\n var idx = 0\n while idx < slots\n var val = palette_bytes.get(idx * 4, 1)\n arr[idx] = tasmota.scale_int(val, 0, 255, min, max)\n idx += 1\n end\n end\n \n return arr\n end\n \n # Get color at a specific index (simplified)\n def _get_color_at_index(idx)\n if idx < 0 || idx >= self._slots\n return 0xFFFFFFFF\n end\n \n var palette_bytes = self._get_palette_bytes()\n var trgb = palette_bytes.get(idx * 4, -4) # Big Endian\n trgb = trgb | 0xFF000000 # set alpha channel to full opaque\n return trgb\n end\n \n # Interpolate a value between two points using the selected transition type\n #\n # @param value: int - Current value to interpolate\n # @param from_min: int - Start of range\n # @param from_max: int - End of range\n # @param to_min: int - Start of output range\n # @param to_max: int - End of output range\n # @return int - Interpolated value\n def _interpolate(value, from_min, from_max, to_min, to_max)\n var transition_type = self.transition_type\n \n if transition_type == 5 #-animation.SINE-#\n # Cosine interpolation for smooth transitions\n # Map value to 0..255 range first\n var t = tasmota.scale_uint(value, from_min, from_max, 0, 255)\n \n # Map to angle range for cosine: 0 -> 16384 (180 degrees)\n # We use cosine from 180\u00b0 to 0\u00b0 which gives us 0->1 smooth curve\n var angle = tasmota.scale_uint(t, 0, 255, 16384, 0)\n \n # tasmota.sine_int returns -4096 to 4096 for angle 0-32767\n # At angle 16384 (180\u00b0): sine_int returns 0 (actually cosine = -1)\n # At angle 0 (0\u00b0): sine_int returns 0 (cosine = 1)\n # We need to shift by 8192 to get cosine behavior\n var cos_val = tasmota.sine_int(angle + 8192) # -4096 to 4096\n \n # Map cosine from -4096..4096 to 0..255\n var normalized = tasmota.scale_int(cos_val, -4096, 4096, 0, 255)\n \n # Finally map to output range\n return tasmota.scale_int(normalized, 0, 255, to_min, to_max)\n else\n # Default to linear interpolation (for LINEAR mode or any unknown type)\n return tasmota.scale_uint(value, from_min, from_max, to_min, to_max)\n end\n end\n \n # Update object state based on current time\n # Subclasses must override this to implement their update logic\n #\n # @param time_ms: int - Current time in milliseconds\n def update(time_ms)\n # Rebuild LUT if dirty\n if self._lut_dirty || self._color_lut == nil\n self._rebuild_color_lut()\n end\n\n # Cache the brightness to an instance variable for this tick\n self._brightness = self.member(\"brightness\")\n end\n\n # Produce a color value for any parameter name (optimized version from Animate_palette)\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds\n # @return int - Color in ARGB format (0xAARRGGBB)\n def produce_value(name, time_ms)\n # Ensure time_ms is valid and initialize start_time if needed\n time_ms = self._fix_time_ms(time_ms)\n\n if (self._slots_arr == nil) && (self._value_arr == nil)\n self._recompute_palette()\n end\n var palette_bytes = self._get_palette_bytes()\n \n if palette_bytes == nil || self._slots < 2\n return 0xFFFFFFFF\n end\n \n # Get parameter values using virtual member access\n var period = self.period\n var brightness = self.brightness\n \n # If period is 0, return static color (first color in palette)\n if period == 0\n var bgrt0 = palette_bytes.get(0, 4)\n var r = (bgrt0 >> 8) & 0xFF\n var g = (bgrt0 >> 16) & 0xFF\n var b = (bgrt0 >> 24) & 0xFF\n \n # Apply brightness scaling (inline for speed)\n if brightness != 255\n r = tasmota.scale_uint(r, 0, 255, 0, brightness)\n g = tasmota.scale_uint(g, 0, 255, 0, brightness)\n b = tasmota.scale_uint(b, 0, 255, 0, brightness)\n end\n \n var final_color = (0xFF << 24) | (r << 16) | (g << 8) | b\n return final_color\n end\n \n # Calculate position in cycle using start_time\n var elapsed = time_ms - self.start_time\n var past = elapsed % period\n \n # Find slot (exact algorithm from Animate_palette)\n var slots = self._slots\n var idx = slots - 2\n while idx > 0\n if past >= self._slots_arr[idx] break end\n idx -= 1\n end\n \n var bgrt0 = palette_bytes.get(idx * 4, 4)\n var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)\n var t0 = self._slots_arr[idx]\n var t1 = self._slots_arr[idx + 1]\n \n # Use interpolation based on transition_type (LINEAR or SINE)\n var r = self._interpolate(past, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)\n var g = self._interpolate(past, t0, t1, (bgrt0 >> 16) & 0xFF, (bgrt1 >> 16) & 0xFF)\n var b = self._interpolate(past, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)\n\n # Apply brightness scaling (inline for speed)\n if brightness != 255\n r = tasmota.scale_uint(r, 0, 255, 0, brightness)\n g = tasmota.scale_uint(g, 0, 255, 0, brightness)\n b = tasmota.scale_uint(b, 0, 255, 0, brightness)\n end\n\n # Create final color in ARGB format\n var final_color = (0xFF << 24) | (r << 16) | (g << 8) | b\n \n return final_color\n end\n \n # Rebuild the color lookup table (129 entries covering 0-255 range)\n #\n # LUT Design:\n # - Entries: 0, 2, 4, 6, ..., 254, 255 (129 entries = 516 bytes)\n # - Covers full 0-255 range with 2-step resolution (ignoring LSB)\n # - Final entry at index 128 stores color for value 255\n # - Colors stored at MAXIMUM brightness (255) - actual brightness applied after lookup\n #\n # Why 2-step resolution?\n # - Reduces memory from 1KB (256 entries) to 516 bytes (129 entries)\n # - Visual quality: 2-step resolution is imperceptible in color gradients\n # - Performance: Still provides ~5-10x speedup over full interpolation\n #\n # Why maximum brightness in LUT?\n # - Allows brightness to change dynamically without invalidating LUT\n # - Actual brightness scaling applied in get_color_for_value() after lookup\n # - Critical for animations where brightness changes over time\n #\n # Storage format:\n # - Uses bytes.set(offset, color, 4) for efficient 32-bit ARGB storage\n # - Little-endian format (native Berry integer representation)\n def _rebuild_color_lut()\n # Ensure palette arrays are initialized\n if self._value_arr == nil\n self._recompute_palette()\n end\n \n # Allocate LUT if needed (129 entries * 4 bytes = 516 bytes)\n if self._color_lut == nil\n self._color_lut = bytes()\n self._color_lut.resize(129 * 4)\n end\n \n # Pre-compute colors for values 0, 2, 4, ..., 254 at max brightness\n var lut_factor = self.LUT_FACTOR # multiplier\n var i = 0\n var i_max = (256 >> lut_factor)\n while i < i_max\n var value = i << lut_factor\n var color = self._get_color_for_value_uncached(value, 0)\n \n # Store color using efficient bytes.set()\n self._color_lut.set(i << 2, color, 4)\n i += 1\n end\n \n # Add final entry for value 255 at max brightness\n var color_255 = self._get_color_for_value_uncached(255, 0)\n self._color_lut.set(i_max << 2, color_255, 4)\n \n self._lut_dirty = false\n end\n \n # Get color for a specific value WITHOUT using cache (internal method)\n # This is the original implementation moved to a separate method\n # Colors are returned at MAXIMUM brightness (255) - brightness scaling applied separately\n #\n # @param value: int/float - Value to map to a color (0-255 range)\n # @param time_ms: int - Current time in milliseconds (ignored for value-based color)\n # @return int - Color in ARGB format at maximum brightness\n def _get_color_for_value_uncached(value, time_ms)\n if (self._slots_arr == nil) && (self._value_arr == nil)\n self._recompute_palette()\n end\n var palette_bytes = self._get_palette_bytes()\n \n # Find slot (exact algorithm from Animate_palette.set_value)\n var slots = self._slots\n var idx = slots - 2\n while idx > 0\n if value >= self._value_arr[idx] break end\n idx -= 1\n end\n \n var bgrt0 = palette_bytes.get(idx * 4, 4)\n var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)\n var t0 = self._value_arr[idx]\n var t1 = self._value_arr[idx + 1]\n \n # Use interpolation based on transition_type (LINEAR or SINE)\n var r = self._interpolate(value, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)\n var g = self._interpolate(value, t0, t1, (bgrt0 >> 16) & 0xFF, (bgrt1 >> 16) & 0xFF)\n var b = self._interpolate(value, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)\n \n # Create final color in ARGB format at maximum brightness\n return (0xFF << 24) | (r << 16) | (g << 8) | b\n end\n \n # Get color for a specific value using LUT cache for performance\n #\n # This is the optimized version that uses the LUT cache instead of\n # performing expensive palette interpolation on every call.\n #\n # Performance characteristics:\n # - LUT lookup: ~10-15 CPU cycles (bit shift + bytes.get())\n # - Original interpolation: ~50-100 CPU cycles (search + interpolate + brightness)\n # - Speedup: ~5-10x faster\n #\n # LUT mapping:\n # - Values 0-254: lut_index = value >> 1 (divide by 2, ignore LSB)\n # - Value 255: lut_index = 128 (special case for exact 255)\n #\n # Brightness handling:\n # - LUT stores colors at maximum brightness (255)\n # - Actual brightness scaling applied here after lookup using static method\n # - This allows brightness to change dynamically without invalidating LUT\n #\n # @param value: int/float - Value to map to a color (0-255 range)\n # @param time_ms: int - Current time in milliseconds (ignored for value-based color)\n # @return int - Color in ARGB format\n def get_color_for_value(value, time_ms)\n # Clamp value to 0-255 range\n # if value < 0 value = 0 end\n # if value > 255 value = 255 end\n \n # Map value to LUT index\n # For values 0-254: index = value / 2 (integer division)\n # For value 255: index = 128\n var lut_index = value >> self.LUT_FACTOR # Divide by 2 using bit shift\n if value >= 255\n lut_index = 128\n end\n \n # Retrieve color from LUT using efficient bytes.get()\n # This color is at maximum brightness (255)\n var color = self._color_lut.get(lut_index * 4, 4)\n \n # Apply brightness scaling if not at maximum\n var brightness = self._brightness\n if brightness != 255\n # Extract RGB components\n var r = (color >> 16) & 0xFF\n var g = (color >> 8) & 0xFF\n var b = color & 0xFF\n \n # Scale each component by brightness\n r = tasmota.scale_uint(r, 0, 255, 0, brightness)\n g = tasmota.scale_uint(g, 0, 255, 0, brightness)\n b = tasmota.scale_uint(b, 0, 255, 0, brightness)\n \n # Reconstruct color with scaled brightness\n color = (0xFF << 24) | (r << 16) | (g << 8) | b\n end\n \n return color\n end\n \n # Generate CSS linear gradient (reused from Animate_palette.to_css_gradient)\n #\n # @return string - CSS linear gradient string\n def to_css_gradient()\n var palette_bytes = self._get_palette_bytes()\n \n if palette_bytes == nil\n return \"background:linear-gradient(to right, #000000);\"\n end\n \n var arr = self._parse_palette(0, 1000)\n var ret = \"background:linear-gradient(to right\"\n var idx = 0\n while idx < size(arr)\n var prm = arr[idx] # per mile\n\n var bgrt = palette_bytes.get(idx * 4, 4)\n var r = (bgrt >> 8) & 0xFF\n var g = (bgrt >> 16) & 0xFF\n var b = (bgrt >> 24) & 0xFF\n ret += f\",#{r:02X}{g:02X}{b:02X} {prm/10.0:.1f}%\"\n idx += 1\n end\n ret += \");\"\n return ret\n end\nend\n\nreturn {'rich_palette_color': rich_palette_color}"; modules["providers/static_value_provider.be"] = "# static_value for Berry Animation Framework\n#\n# This value provider returns a single, static value for any parameter type.\n# It's a dummy implementation that serves as a wrapper for static values,\n# providing the same interface as dynamic value providers.\n#\n# This provider uses the member() construct to respond to any get_XXX() method\n# call with the same static value, making it a universal static provider.\n#\n# Follows the parameterized class specification:\n# - Constructor takes only 'engine' parameter\n# - Value is set via virtual member assignment after creation\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass static_value : animation.parameterized_object\n static var VALUE_PROVIDER = true\n # Parameter definitions\n static var PARAMS = animation.enc_params({\n \"value\": {\"default\": nil, \"type\": \"any\"}\n })\n \n\n # Produce the static value for any parameter name\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds (ignored)\n # @return any - The static value\n def produce_value(name, time_ms)\n return self.value\n end\nend\n\nreturn {'static_value': static_value}"; modules["providers/strip_length_provider.be"] = "# strip_length for Berry Animation Framework\n#\n# This value provider returns the length of the LED strip from the animation engine.\n# It provides access to the strip length as a dynamic value that can be used by\n# animations that need to know the strip dimensions.\n#\n# The strip length is obtained from the engine's width property, which is cached\n# from the strip.length() method for performance.\n#\n# Follows the parameterized class specification:\n# - Constructor takes only 'engine' parameter\n# - No additional parameters needed since strip length is obtained from engine\n\nclass strip_length : animation.parameterized_object\n static var VALUE_PROVIDER = true\n # Produce the strip length value\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds (ignored)\n # @return int - The strip length in pixels\n def produce_value(name, time_ms)\n return self.engine.strip_length\n end\nend\n\nreturn {'strip_length': strip_length}"; modules["providers/value_provider.be"] = "# value_provider utilities for Berry Animation Framework\n#\n# Value providers are parameterized_object subclasses with VALUE_PROVIDER = true.\n# They generate values based on time, which can be used by animations\n# for any parameter that needs to be dynamic over time.\n#\n# The value_provider class has been removed from the hierarchy.\n# All value providers now inherit directly from parameterized_object\n# and set `static var VALUE_PROVIDER = true`.\n\n# Check if an object is a value provider\n# Returns true if obj is a parameterized_object with VALUE_PROVIDER = true\ndef is_value_provider(obj)\n return obj != nil && type(obj) == \"instance\" && isinstance(obj, animation.parameterized_object) && obj.VALUE_PROVIDER\nend\n\nreturn {'is_value_provider': is_value_provider}\n"; modules["providers_future/composite_color_provider.be"] = "# CompositeColorProvider for Berry Animation Framework\n#\n# This color provider combines multiple color providers with blending.\n# It allows for creating complex color effects by layering simpler ones.\n#\n# Follows the parameterized class specification:\n# - Constructor takes only 'engine' parameter\n# - All other parameters set via virtual member assignment after creation\n\nimport \"./core/param_encoder\" as encode_constraints\n\nclass CompositeColorProvider : animation.color_provider\n # Non-parameter instance variables only\n var providers # List of color providers\n \n # Parameter definitions\n static var PARAMS = animation.enc_params({\n \"blend_mode\": {\"enum\": [0, 1, 2], \"default\": 0} # 0=overlay, 1=add, 2=multiply\n })\n \n # Initialize a new CompositeColorProvider\n #\n # @param engine: AnimationEngine - Reference to the animation engine (required)\n def init(engine)\n super(self).init(engine) # Initialize parameter system\n \n # Initialize non-parameter instance variables\n self.providers = []\n end\n \n # Add a provider to the list\n #\n # @param provider: color_provider - Provider to add\n # @return self for method chaining\n def add_provider(provider)\n self.providers.push(provider)\n return self\n end\n \n # Produce a composite color for any parameter name\n #\n # @param name: string - Parameter name being requested (ignored)\n # @param time_ms: int - Current time in milliseconds\n # @return int - Color in ARGB format (0xAARRGGBB)\n def produce_value(name, time_ms)\n if size(self.providers) == 0\n return 0xFFFFFFFF # Default to white\n end\n \n if size(self.providers) == 1\n var color = self.providers[0].produce_value(name, time_ms)\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\n \n var result_color = self.providers[0].produce_value(name, time_ms)\n \n var i = 1\n while i < size(self.providers)\n var next_color = self.providers[i].produce_value(name, time_ms)\n result_color = self._blend_colors(result_color, next_color)\n i += 1\n end\n \n # Apply brightness scaling to final composite color\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(result_color, brightness)\n end\n return result_color\n end\n \n # Get a composite color based on a value\n #\n # @param value: int/float - Value to map to a color (0-255 range)\n # @param time_ms: int - Current time in milliseconds\n # @return int - Color in ARGB format (0xAARRGGBB)\n def get_color_for_value(value, time_ms)\n if size(self.providers) == 0\n return 0xFFFFFFFF # Default to white\n end\n \n if size(self.providers) == 1\n var color = self.providers[0].get_color_for_value(value, time_ms)\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(color, brightness)\n end\n return color\n end\n \n var result_color = self.providers[0].get_color_for_value(value, time_ms)\n \n var i = 1\n while i < size(self.providers)\n var next_color = self.providers[i].get_color_for_value(value, time_ms)\n result_color = self._blend_colors(result_color, next_color)\n i += 1\n end\n \n # Apply brightness scaling to final composite color\n var brightness = self.brightness\n if brightness != 255\n return self.apply_brightness(result_color, brightness)\n end\n return result_color\n end\n \n # Blend two colors based on the blend mode\n #\n # @param color1: int - First color (32-bit ARGB)\n # @param color2: int - Second color (32-bit ARGB)\n # @return int - Blended color (32-bit ARGB)\n def _blend_colors(color1, color2)\n var blend_mode = self.blend_mode\n \n var a1 = (color1 >> 24) & 0xFF\n var b1 = (color1 >> 16) & 0xFF\n var g1 = (color1 >> 8) & 0xFF\n var r1 = color1 & 0xFF\n \n var a2 = (color2 >> 24) & 0xFF\n var b2 = (color2 >> 16) & 0xFF\n var g2 = (color2 >> 8) & 0xFF\n var r2 = color2 & 0xFF\n \n var a, r, g, b\n \n if blend_mode == 0 # Overlay\n var alpha = a2 / 255.0\n r = int(r1 * (1 - alpha) + r2 * alpha)\n g = int(g1 * (1 - alpha) + g2 * alpha)\n b = int(b1 * (1 - alpha) + b2 * alpha)\n a = a1 > a2 ? a1 : a2\n elif blend_mode == 1 # Add\n r = r1 + r2\n g = g1 + g2\n b = b1 + b2\n a = a1 > a2 ? a1 : a2\n \n # Clamp values\n r = r > 255 ? 255 : r\n g = g > 255 ? 255 : g\n b = b > 255 ? 255 : b\n elif blend_mode == 2 # Multiply\n r = tasmota.scale_uint(r1 * r2, 0, 255 * 255, 0, 255)\n g = tasmota.scale_uint(g1 * g2, 0, 255 * 255, 0, 255)\n b = tasmota.scale_uint(b1 * b2, 0, 255 * 255, 0, 255)\n a = a1 > a2 ? a1 : a2\n end\n \n return (a << 24) | (b << 16) | (g << 8) | r\n end\nend\n\nreturn {'composite_color': CompositeColorProvider}"; modules["tasmota.be"] = "#!/usr/bin/env -S ./berry -s -g\n#\n# autoexec.be\n#\n# auto-load all Tasmota environment and run code\n\n# add `tasmota_env` in the path for importing modules\n# we do it through an anonymous function to not pollute the global namespace\ndo\n import sys\n var path = sys.path()\n path.push('./tasmota_env')\nend\n\n# import common modules that are auto-imported in Tasmota\nimport global\ndo\n global.global = global\n # import global as global_inner\n # global_inner.global = global_inner\nend\n\n# import all Tasmota emulator stuff\ndo\n import load\n global.load = load\nend\n\nglobal.tasmota = nil # make sure it's visible in global scope\n\nimport gpio\nglobal.gpio = gpio\nimport light_state\nglobal.light_state = light_state\nimport Leds\nglobal.Leds = Leds\nimport tasmota_core as tasmota\nglobal.tasmota = tasmota\n\nreturn tasmota\n"; modules["tasmota_env/Leds.be"] = "# Leds\n\n\nclass Leds\n static var WS2812_GRB = 1\n static var SK6812_GRBW = 2\n\n var gamma # if true, apply gamma (true is default)\n var leds # number of leds\n var bri # implicit brightness for this led strip (0..255, default is 50% = 127)\n\n var _buf\n var _typ\n # leds:int = number of leds of the strip\n # gpio:int (optional) = GPIO for NeoPixel. If not specified, takes the WS2812 gpio\n # typ:int (optional) = Type of LED, defaults to WS2812 RGB\n # rmt:int (optional) = RMT hardware channel to use, leave default unless you have a good reason \n def init(leds, gpio_phy, typ, rmt) # rmt is optional\n self.gamma = false # force no gamma for __JS__\n \n # In browser mode, get strip size from JavaScript if not explicitly provided\n if leds == nil && global.contains('__JS__')\n import js\n var js_size = int(js.get_strip_size())\n if js_size > 0\n leds = js_size\n end\n end\n \n self.leds = (leds != nil) ? int(leds) : 30\n self.bri = 255 # force 255 for __JS__\n\n # fake buffer\n self._buf = bytes(self.leds).resize(self.leds * 3)\n self._typ = typ\n # if no GPIO, abort\n # if gpio_phy == nil\n # raise \"valuer_error\", \"no GPIO specified for neopixelbus\"\n # end\n\n # initialize the structure\n self.ctor(self.leds, gpio_phy, typ, rmt)\n\n # if self._p == nil raise \"internal_error\", \"couldn't not initialize noepixelbus\" end\n\n # call begin\n self.begin()\n\n # emulator-specific\n global._strip = self # record the current strip object\n\n end\n\n # set bri (0..255)\n # set bri (0..511)\n def set_bri(bri)\n if (bri < 0) bri = 0 end\n if (bri > 511) bri = 511 end\n self.bri = bri\n end\n def get_bri()\n # If running in browser, get brightness from JavaScript UI\n # JavaScript brightness is 0-200 where 100 = normal (255 in Berry)\n # We scale: 0 -> 0, 100 -> 255, 200 -> 510 (allows overexpose)\n # Note: we map to 0-510 (not 0-511) so that 100 maps exactly to 255\n if global.contains('__JS__')\n import js\n var js_bri = int(js.get_brightness())\n # Scale: js_bri 0-200 maps to 0-510 so that 100 -> 255 exactly\n return tasmota.scale_uint(js_bri, 0, 200, 0, 510)\n end\n return self.bri\n end\n\n def set_gamma(gamma)\n self.gamma = bool(gamma)\n end\n def get_gamma()\n return self.gamma\n end\n\n # assign RMT\n static def assign_rmt(gpio_phy)\n end\n\n def clear()\n self.clear_to(0x000000)\n self.show()\n end\n\n def ctor(leds, gpio_phy, typ, rmt)\n if typ == nil\n typ = self.WS2812_GRB\n end\n self._typ = typ\n # if rmt == nil\n # rmt = self.assign_rmt(gpio_phy)\n # end\n # self.call_native(0, leds, gpio_phy, typ, rmt)\n end\n def begin()\n end\n def show()\n # Display frame buffer to JavaScript (browser only)\n # This sends the LED strip pixel data to JavaScript for rendering\n if global.contains('__JS__')\n import js\n js.frame_buffer_display(self._buf.tohex())\n end\n end\n def can_show()\n return true\n end\n def is_dirty()\n return true\n end\n def dirty()\n end\n\n # push_pixels\n #\n # Pushes a bytes() buffer of 0xAARRGGBB colors, without bri nor gamma correction\n # \n def push_pixels_buffer_argb(pixels)\n var i = 0\n while i < self.pixel_count()\n self.set_pixel_color(i, pixels.get(i * 4, 4))\n i += 1\n end\n end\n\n def pixels_buffer(old_buf)\n return self._buf\n end\n def pixel_size()\n return self.call_native(7)\n end\n def pixel_count()\n # If running in browser, get strip size from JavaScript\n if global.contains('__JS__')\n import js\n var js_size = int(js.get_strip_size())\n if js_size > 0\n # Resize internal buffer if needed\n if js_size != self.leds\n self.leds = js_size\n self._buf = bytes(js_size * 3)\n self._buf.resize(js_size * 3)\n end\n return js_size\n end\n end\n return self.leds\n # return self.call_native(8)\n end\n def length()\n return self.pixel_count()\n end\n def pixel_offset()\n return 0\n end\n def clear_to(col, bri)\n if (bri == nil) bri = self.get_bri() end\n var rgb = self.to_gamma(col, bri)\n var buf = self._buf\n var r = (rgb >> 16) & 0xFF\n var g = (rgb >> 8) & 0xFF\n var b = (rgb ) & 0xFF\n var i = 0\n var count = self.pixel_count() # Use pixel_count() to trigger buffer resize if needed\n while i < count\n buf[i * 3 + 0] = r\n buf[i * 3 + 1] = g\n buf[i * 3 + 2] = b\n i += 1\n end\n end\n def set_pixel_color(idx, col, bri)\n if (bri == nil) bri = self.get_bri() end\n var rgb = self.to_gamma(col, bri)\n var buf = self._buf\n var r = (rgb >> 16) & 0xFF\n var g = (rgb >> 8) & 0xFF\n var b = (rgb ) & 0xFF\n buf[idx * 3 + 0] = r\n buf[idx * 3 + 1] = g\n buf[idx * 3 + 2] = b\n #self.call_native(10, idx, self.to_gamma(col, bri))\n end\n def get_pixel_color(idx)\n var r = self._buf[idx * 3 + 0]\n var g = self._buf[idx * 3 + 1]\n var b = self._buf[idx * 3 + 2]\n return (r << 16) | (g << 8) | b\n # return self.call_native(11, idx)\n end\n\n # apply gamma and bri\n def to_gamma(rgb, bri255)\n if (bri255 == nil) bri255 = self.bri end\n return self.apply_bri_gamma(rgb, bri255, self.gamma)\n end\n\n # `segment`\n # create a new `strip` object that maps a part of the current strip\n def create_segment(offset, leds)\n if int(offset) + int(leds) > self.leds || offset < 0 || leds < 0\n raise \"value_error\", \"out of range\"\n end\n\n # inner class\n class Leds_segment\n var strip\n var offset, leds\n \n def init(strip, offset, leds)\n self.strip = strip\n self.offset = int(offset)\n self.leds = int(leds)\n end\n \n def clear()\n self.clear_to(0x000000)\n self.show()\n end\n \n def begin()\n # do nothing, already being handled by physical strip\n end\n def show(force)\n # don't trigger on segment, you will need to trigger on full strip instead\n if bool(force) || (self.offset == 0 && self.leds == self.strip.leds)\n self.strip.show()\n end\n end\n def can_show()\n return self.strip.can_show()\n end\n def is_dirty()\n return self.strip.is_dirty()\n end\n def dirty()\n self.strip.dirty()\n end\n def pixels_buffer()\n return nil\n end\n def pixel_size()\n return self.strip.pixel_size()\n end\n def pixel_offset()\n return self.offset\n end\n def pixel_count()\n return self.leds\n end\n def clear_to(col, bri)\n if (bri == nil) bri = self.bri end\n self.strip.call_native(9, self.strip.to_gamma(col, bri), self.offset, self.leds)\n # var i = 0\n # while i < self.leds\n # self.strip.set_pixel_color(i + self.offset, col, bri)\n # i += 1\n # end\n end\n def set_pixel_color(idx, col, bri)\n if (bri == nil) bri = self.bri end\n self.strip.set_pixel_color(idx + self.offset, col, bri)\n end\n def get_pixel_color(idx)\n return self.strip.get_pixel_color(idx + self.offseta)\n end\n end\n\n return Leds_segment(self, offset, leds)\n\n end\n\n def create_matrix(w, h, offset)\n offset = int(offset)\n w = int(w)\n h = int(h)\n if offset == nil offset = 0 end\n if w * h + offset > self.leds || h < 0 || w < 0 || offset < 0\n raise \"value_error\", \"out of range\"\n end\n\n # inner class\n class Leds_matrix\n var strip\n var offset\n var h, w\n var alternate # are rows in alternate mode (even/odd are reversed)\n var pix_buffer\n var pix_size\n \n def init(strip, w, h, offset)\n self.strip = strip\n self.offset = offset\n self.h = h\n self.w = w\n self.alternate = false\n\n self.pix_buffer = self.strip.pixels_buffer()\n self.pix_size = self.strip.pixel_size()\n end\n \n def clear()\n self.clear_to(0x000000)\n self.show()\n end\n \n def begin()\n # do nothing, already being handled by physical strip\n end\n def show(force)\n # don't trigger on segment, you will need to trigger on full strip instead\n if bool(force) || (self.offset == 0 && self.w * self.h == self.strip.leds)\n self.strip.show()\n self.pix_buffer = self.strip.pixels_buffer(self.pix_buffer) # update buffer after show()\n end\n end\n def can_show()\n return self.strip.can_show()\n end\n def is_dirty()\n return self.strip.is_dirty()\n end\n def dirty()\n self.strip.dirty()\n end\n def pixels_buffer()\n return self.strip.pixels_buffer()\n end\n def pixel_size()\n return self.pix_size\n end\n def pixel_count()\n return self.w * self.h\n end\n def pixel_offset()\n return self.offset\n end\n def clear_to(col, bri)\n if (bri == nil) bri = self.bri end\n self.strip.call_native(9, self.strip.to_gamma(col, bri), self.offset, self.w * self.h)\n end\n def set_pixel_color(idx, col, bri)\n if (bri == nil) bri = self.bri end\n self.strip.set_pixel_color(idx + self.offset, col, bri)\n end\n def get_pixel_color(idx)\n return self.strip.get_pixel_color(idx + self.offseta)\n end\n\n # setbytes(row, bytes)\n # sets the raw bytes for `row`, copying at most 3 or 4 x col bytes\n def set_bytes(row, buf, offset, len)\n var h_bytes = self.h * self.pix_size\n if (len > h_bytes) len = h_bytes end\n var offset_in_matrix = (self.offset + row) * h_bytes\n self.pix_buffer.setbytes(offset_in_matrix, buf, offset, len)\n end\n\n # Leds_matrix specific\n def set_alternate(alt)\n self.alternate = alt\n end\n def get_alternate()\n return self.alternate\n end\n\n def set_matrix_pixel_color(x, y, col, bri)\n if (bri == nil) bri = self.bri end\n if self.alternate && x % 2\n # reversed line\n self.strip.set_pixel_color(x * self.w + self.h - y - 1 + self.offset, col, bri)\n else\n self.strip.set_pixel_color(x * self.w + y + self.offset, col, bri)\n end\n end\n end\n\n return Leds_matrix(self, w, h, offset)\n\n end\n\n static def matrix(w, h, gpio, rmt)\n var strip = Leds(w * h, gpio, rmt)\n var matrix = strip.create_matrix(w, h, 0)\n return matrix\n end\n\n\n static def blend_color(color_a, color_b, alpha)\n var transparency = (color_b >> 24) & 0xFF\n if (alpha != nil)\n transparency = 255 - alpha\n end\n # remove any transparency\n color_a = color_a & 0xFFFFFF\n color_b = color_b & 0xFFFFFF\n\n if (transparency == 0) # // color_b is opaque, return color_b\n return color_b\n end\n if (transparency >= 255) #{ // color_b is transparent, return color_a\n return color_a\n end\n var r = tasmota.scale_uint(transparency, 0, 255, (color_b >> 16) & 0xFF, (color_a >> 16) & 0xFF)\n var g = tasmota.scale_uint(transparency, 0, 255, (color_b >> 8) & 0xFF, (color_a >> 8) & 0xFF)\n var b = tasmota.scale_uint(transparency, 0, 255, (color_b ) & 0xFF, (color_a ) & 0xFF)\n\n var rgb = (r << 16) | (g << 8) | b\n return rgb\n end\n\n static def apply_bri_gamma(color_a, bri255, gamma)\n if (bri255 == nil) bri255 = 255 end\n if (bri255 == 0) return 0x000000 end # if bri is zero, short-cut\n var r = (color_a >> 16) & 0xFF\n var g = (color_a >> 8) & 0xFF\n var b = (color_a ) & 0xFF\n\n # Apply brightness scaling\n # bri255 0-255: scale down (0=off, 255=full)\n # bri255 256-510: scale up (overexpose, capped at 255 per channel)\n if (bri255 < 255)\n # Scale down\n r = tasmota.scale_uint(bri255, 0, 255, 0, r)\n g = tasmota.scale_uint(bri255, 0, 255, 0, g)\n b = tasmota.scale_uint(bri255, 0, 255, 0, b)\n elif (bri255 > 255)\n # Scale up (overexpose) - bri255 256-510 maps to 1.0x-2.0x multiplier\n r = tasmota.scale_uint(bri255, 255, 510, r, r * 2)\n g = tasmota.scale_uint(bri255, 255, 510, g, g * 2)\n b = tasmota.scale_uint(bri255, 255, 510, b, b * 2)\n # Cap at 255\n if (r > 255) r = 255 end\n if (g > 255) g = 255 end\n if (b > 255) b = 255 end\n end\n\n if gamma\n import light_state\n r = light_state.ledGamma8_8(r)\n g = light_state.ledGamma8_8(g)\n b = light_state.ledGamma8_8(b)\n end\n var rgb = (r << 16) | (g << 8) | b\n return rgb\n end\n\n \nend\n\nreturn Leds\n"; modules["tasmota_env/Leds_frame.be"] = "# Leds_frame\nimport Leds # solve import\n\nclass Leds_frame : bytes\n var pixel_size\n\n def init(pixels)\n if (pixels < 0) pixels = -pixels end\n self.pixel_size = pixels\n super(self).init(pixels * (-4))\n end\n\n def item(i)\n return self.get(i * 4, 4)\n end\n\n def setitem(i, v)\n self.set(i * 4, v, 4)\n end\n\n def set_pixel(i, r, g, b, alpha)\n if (alpha == nil) alpha = 0 end\n var color = ((alpha & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)\n self.setitem(i, color)\n end\n\n def fill_pixels(color, start_pos, end_pos)\n if (start_pos == nil) start_pos = 0 end\n if (end_pos == nil) end_pos = self.size() / 4 end\n while start_pos < end_pos\n print(f\"+ {start_pos=} {end_pos=}\")\n self.set(start_pos * 4, color, 4)\n start_pos += 1\n end\n end\n\n static def blend(color_a, color_b, alpha)\n var r = (color_a >> 16) & 0xFF\n var g = (color_a >> 8) & 0xFF\n var b = (color_a ) & 0xFF\n var r2 = (color_b >> 16) & 0xFF\n var g2 = (color_b >> 8) & 0xFF\n var b2 = (color_b ) & 0xFF\n var r3 = tasmota.scale_uint(alpha, 0, 255, r2, r)\n var g3 = tasmota.scale_uint(alpha, 0, 255, g2, g)\n var b3 = tasmota.scale_uint(alpha, 0, 255, b2, b)\n var rgb = (r3 << 16) | (g3 << 8) | b3\n return rgb\n end\n\n def paste_pixels(dest_buf, bri, gamma)\n if (bri == nil) bri = 255 end\n if (gamma == nil) gamma = false end\n var pixels_count = self.size() / 4\n if (pixels_count > size(dest_buf) / 3) pixels_count = size(dest_buf) / 3 end\n if (pixels_count > 0)\n var i = 0\n while i < pixels_count\n var src_argb = Leds.apply_bri_gamma(self.get(i * 4, 4), bri, gamma)\n var src_r = (src_argb >> 16) & 0xFF\n var src_g = (src_argb >> 8) & 0xFF\n var src_b = (src_argb ) & 0xFF\n dest_buf[i * 3 + 0] = src_g\n dest_buf[i * 3 + 1] = src_r\n dest_buf[i * 3 + 2] = src_b\n i += 1\n end\n end\n end\n\n def blend_pixels(fore)\n var back = self\n var dest = self\n var dest_len = size(dest)\n if (size(fore) < dest_len) dest_len = size(fore) end\n if (size(back) < dest_len) dest_len = size(back) end\n var pixels_count = dest_len / 4\n\n if (pixels_count > 0)\n var i = 0\n while i < pixels_count\n var back_argb = back.get(i * 4, 4)\n var fore_argb = fore.get(i * 4, 4)\n var fore_alpha = (fore_argb >> 24) & 0xFF\n var dest_rgb_new = back_argb\n\n if (fore_alpha == 0) # { // opaque layer, copy value from fore\n dest_rgb_new = fore_argb\n elif (fore_alpha == 255) # { // fore is transparent, use back\n # // nothing to do, dest_rgb_new = back_argb above\n else\n var back_r = (back_argb >> 16) & 0xFF\n var fore_r = (fore_argb >> 16) & 0xFF\n var back_g = (back_argb >> 8) & 0xFF\n var fore_g = (fore_argb >> 8) & 0xFF\n var back_b = (back_argb ) & 0xFF\n var fore_b = (fore_argb ) & 0xFF\n var dest_r_new = tasmota.scale_uint(fore_alpha, 0, 255, fore_r, back_r)\n var dest_g_new = tasmota.scale_uint(fore_alpha, 0, 255, fore_g, back_g)\n var dest_b_new = tasmota.scale_uint(fore_alpha, 0, 255, fore_b, back_b)\n dest_rgb_new = (dest_r_new << 16) | (dest_g_new << 8) | dest_b_new\n end\n dest.set(i * 4, dest_rgb_new, 4)\n i += 1\n end\n end\n end\n\nend\n\nreturn Leds_frame\n\n# /* @const_object_info_begin\n# class be_class_Leds_frame (scope: global, name: Leds_frame, super:be_class_bytes, strings: weak) {\n# pixel_size, var\n\n# init, closure(Leds_frame_be_init_closure)\n\n# item, closure(Leds_frame_be_item_closure)\n# setitem, closure(Leds_frame_be_setitem_closure)\n# set_pixel, closure(Leds_frame_be_set_pixel_closure)\n\n# // the following are on buffers\n# blend, static_func(be_leds_blend)\n# fill_pixels, func(be_leds_fill_pixels)\n# blend_pixels, func(be_leds_blend_pixels)\n# paste_pixels, func(be_leds_paste_pixels)\n# }"; modules["tasmota_env/gpio.be"] = "\nvar gpio = module('gpio')\n\ngpio.pin = def () return 0 end\n\ngpio.WS2812 = 1\n\nreturn gpio\n"; modules["tasmota_env/light_state.be"] = "# light_state\n\nclass light_state\n static var RELAY = 0\n static var DIMMER = 1\n static var CT = 2\n static var RGB = 3\n static var RGBW = 4\n static var RGBCT = 5\n\n var channels # number of channels\n\n var power # (bool) on/off state\n var reachable\t # (bool) light is reachable\n var type # (int) number of channels of the light\n var bri # (int) brightness of the light (0..255)\n var ct # (int) white temperature of the light (153..500)\n var sat # (int) saturation of the light (0..255)\n var hue # (int) hue of the light (0..360)\n var hue16 # (int) hue as 16 bits (0..65535)\n var r, g, b # (int) Red Green Blue channels (0..255)\n var r255, g255, b255 # (int) Red Green Blue channels (0..255) at full brightness\n var x,y # (float) x/y color as floats (0.0 .. 1.0)\n var mode_ct, mode_rgb # (bool) light is in RGB or CT mode\n\n static var _gamma_table = [\n [ 1, 1 ],\n [ 4, 1 ],\n [ 209, 13 ],\n [ 312, 41 ],\n [ 457, 106 ],\n [ 626, 261 ],\n [ 762, 450 ],\n [ 895, 703 ],\n [ 1023, 1023 ],\n [ 0xFFFF, 0xFFFF ] # fail-safe if out of range\n ]\n\n\n def init(channels)\n self.power = false\n self.reachable = true\n self.type = channels\n self.bri = 0\n self.ct = 153\n self.sat = 255\n self.hue = 0\n self.hue16 = 0\n self.r255 = 255\n self.g255 = 255\n self.b255 = 255\n self.r = 0\n self.g = 0\n self.b = 0\n self.x = 0.5\n self.y = 0.5\n self.mode_ct = false\n self.mode_rgb = true\n end\n\n #\n # INTERNAL\n #\n def signal_change() end # nop\n\n # compatibility\n static def gamma8(v)\n return _class.ledGamma8_8(v)\n end\n static def gamma10(v)\n return _class.ledGamma10_10(v)\n end\n\n #\n # GAMMA\n #\n # 10 bits in, 10 bits out\n static def ledGamma10_10(v)\n return _class.ledGamma_internal(v, _class._gamma_table)\n end\n\n static def ledGamma8_8(v8)\n if (v8 <= 0) return 0 end\n var v10 = tasmota.scale_uint(v8, 0, 255, 0, 1023)\n var g10 = _class.ledGamma10_10(v10)\n var g8 = tasmota.scale_uint(g10, 4, 1023, 1, 255)\n return g8\n end\n\n # Calculate the gamma corrected value for LEDS\n static def ledGamma_internal(v, gt_ptr)\n var from_src = 0\n var from_gamma = 0\n \n var idx = 0\n while true\n var gt = gt_ptr[idx]\n var to_src = gt[0]\n var to_gamma = gt[1]\n if (v <= to_src)\n return tasmota.scale_uint(v, from_src, to_src, from_gamma, to_gamma)\n end\n from_src = to_src\n from_gamma = to_gamma\n idx += 1\n end\n end\n\n def set_rgb(r,g,b)\n var maxi = (r > g && r > b) ? r : (g > b) ? g : b # // 0..255\n\n if (0 == maxi)\n r = 255\n g = 255\n b = 255\n #self.mode_ct = false\n #self.mode_rgb = true\n #setColorMode(LCM_CT); // try deactivating RGB, setColorMode() will check if this is legal\n else\n if (255 > maxi)\n #// we need to normalize rgb\n r = tasmota.scale_uint(r, 0, maxi, 0, 255)\n g = tasmota.scale_uint(g, 0, maxi, 0, 255)\n b = tasmota.scale_uint(b, 0, maxi, 0, 255)\n end\n # addRGBMode();\n end\n\n self.r255 = r\n self.g255 = g\n self.b255 = b\n self.compute_rgb()\n self.RgbToHsb(r,g,b)\n end\n\n def RgbToHsb(r,g,b)\n #RgbToHsb(r, g, b, &hue, &sat, nullptr);\n # void RgbToHsb(uint8_t ir, uint8_t ig, uint8_t ib, uint16_t *r_hue, uint8_t *r_sat, uint8_t *r_bri) {\n var max = (r > g && r > b) ? r : (g > b) ? g : b # // 0..255\n var min = (r < g && r < b) ? r : (g < b) ? g : b # // 0..255\n var d = max - min # // 0..255\n \n var hue = 0 #; // hue value in degrees ranges from 0 to 359\n var sat = 0 #; // 0..255\n var bri = max #; // 0..255\n \n if (d != 0)\n sat = tasmota.scale_uint(d, 0, max, 0, 255)\n if (r == max)\n hue = (g > b) ? tasmota.scale_uint(g-b,0,d,0,60) : 360 - tasmota.scale_uint(b-g,0,d,0,60)\n elif (g == max)\n hue = (b > r) ? 120 + tasmota.scale_uint(b-r,0,d,0,60) : 120 - tasmota.scale_uint(r-b,0,d,0,60)\n else\n hue = (r > g) ? 240 + tasmota.scale_uint(r-g,0,d,0,60) : 240 - tasmota.scale_uint(g-r,0,d,0,60)\n end\n hue = hue % 360 #; // 0..359\n end\n \n self.hue = hue\n self.hue16 = tasmota.scale_uint(hue, 0, 360, 0, 65535);\n self.sat = sat\n self.bri = bri\n\n end\n\n # convert r255 to r accodring to bri\n def compute_rgb()\n self.r = tasmota.scale_uint(self.r255, 0, 255, 0, self.bri)\n self.g = tasmota.scale_uint(self.g255, 0, 255, 0, self.bri)\n self.b = tasmota.scale_uint(self.b255, 0, 255, 0, self.bri)\n end\n\n def set_bri(bri)\n if (bri == nil) bri = 0 end\n if (bri < 0) bri = 0 end\n if (bri > 255) bri = 255 end\n self.bri = bri\n self.compute_rgb()\n end\n\n def HsToRgb(hue, sat)\n #void HsToRgb(uint16_t hue, uint8_t sat, uint8_t *r_r, uint8_t *r_g, uint8_t *r_b) {\n var r = 255\n var g = 255\n var b = 255\n # // we take brightness at 100%, brightness should be set separately\n hue = hue % 360 #; // normalize to 0..359\n\n if (sat > 0)\n var i = hue / 60 #; // quadrant 0..5\n var f = hue % 60 #; // 0..59\n var q = 255 - tasmota.scale_uint(f, 0, 60, 0, sat) # // 0..59\n var p = 255 - sat\n var t = 255 - tasmota.scale_uint(60 - f, 0, 60, 0, sat)\n\n if i == 0\n # //r = 255;\n g = t\n b = p\n elif i == 1\n r = q\n # //g = 255;\n b = p\n elif i == 2\n r = p\n # //g = 255;\n b = t\n elif i == 3\n r = p\n g = q\n # //b = 255;\n elif i == 4\n r = t\n g = p\n # //b = 255;\n else\n # //r = 255;\n g = p\n b = q\n end\n self.r = r\n self.g = g\n self.b = b\n end\n end\n\nend\nreturn light_state\n\n#-\n\nvar tasmota = compile(\"tasmota.be\",\"file\")()()\nvar light_state = compile(\"light_state.be\",\"file\")()\n\nassert(tasmota.scale_int(10,-500,500,5000,-5000) == -100)\nassert(tasmota.scale_int(0,-500,500,5000,-5000) == 0)\nassert(tasmota.scale_int(-500,-500,500,5000,-5000) == 5000)\nassert(tasmota.scale_int(450,-500,500,5000,-5000) == -4500)\n\nassert(light_state.ledGamma10_10(0) == 0)\nassert(light_state.ledGamma10_10(1) == 1)\nassert(light_state.ledGamma10_10(10) == 1)\nassert(light_state.ledGamma10_10(45) == 3)\nassert(light_state.ledGamma10_10(500) == 145)\nassert(light_state.ledGamma10_10(1020) == 1016)\nassert(light_state.ledGamma10_10(1023) == 1023)\n\n-#\n"; modules["tasmota_env/load.be"] = "# load()\n\ndef load(filename, globalname)\n import global\n var code = compile(filename, \"file\")\n var res = code()\n if (globalname != nil)\n global.(globalname) = res\n end\n return res\nend\n\nreturn load\n"; modules["tasmota_env/tasmota_core.be"] = "# Tasmota emulator - lightweitght for Leds animation\n\nimport global\n\nclass Tasmota\n var _millis # emulate millis from Tasmota\n var _fl # fast_loop\n\n def init()\n self._millis = 1\n end\n\n static def scale_uint(inum, ifrom_min, ifrom_max, ito_min, ito_max)\n if (ifrom_min >= ifrom_max)\n return (ito_min > ito_max ? ito_max : ito_min) # invalid input, return arbitrary value\n end\n \n var num = inum\n var from_min = ifrom_min\n var from_max = ifrom_max\n var to_min = ito_min\n var to_max = ito_max\n\n # check source range\n num = (num > from_max ? from_max : (num < from_min ? from_min : num))\n\n # check to_* order\n if (to_min > to_max)\n # reverse order\n num = (from_max - num) + from_min\n to_min = ito_max\n to_max = ito_min\n end\n\n # short-cut if limits to avoid rounding errors\n if (num == from_min) return to_min end\n if (num == from_max) return to_max end\n \n var result\n if ((num - from_min) < 0x8000)\n if (to_max - to_min > from_max - from_min)\n var numerator = (num - from_min) * (to_max - to_min) * 2\n result = ((numerator / (from_max - from_min)) + 1 ) / 2 + to_min\n else\n var numerator = ((num - from_min) * 2 + 1) * (to_max - to_min + 1)\n result = numerator / ((from_max - from_min + 1) * 2) + to_min\n end\n else\n var numerator = (num - from_min) * (to_max - to_min + 1)\n result = numerator / (from_max - from_min) + to_min\n end\n\n return (result > to_max ? to_max : (result < to_min ? to_min : result))\n end\n\n static def scale_int(num, from_min, from_max, to_min, to_max)\n # guard-rails\n if (from_min >= from_max)\n return (to_min > to_max ? to_max : to_min) # invalid input, return arbitrary value\n end\n\n var from_offset = 0\n if (from_min < 0)\n from_offset = - from_min\n end\n var to_offset = 0\n if (to_min < 0)\n to_offset = - to_min\n end\n if (to_max < (- to_offset))\n to_offset = - to_max\n end\n\n return _class.scale_uint(num + from_offset, from_min + from_offset, from_max + from_offset, to_min + to_offset, to_max + to_offset) - to_offset\n end\n\n def millis(offset)\n var base_millis\n # In browser environment, use JavaScript's high-resolution timing\n if global.contains('__JS__')\n import js\n base_millis = int(js.call('tasmota_millis'))\n else\n base_millis = self._millis\n end\n return base_millis + (offset == nil ? 0 : offset)\n end\n\n # does nothing\n def add_driver()\n end\n\n def is_network_up()\n return false\n end\n\n def log(m, l)\n if (l == nil) || (l <= 2) # only print for level 2 or below\n print(m)\n end\n end\n\n # internal debugging function\n def set_millis(m)\n self._millis = m\n end\n\n def time_reached(t)\n # naive implementation because the emulator will not run over 20 days\n return (t <= self._millis)\n end\n\n # fast_loop() is a trimmed down version of event() called at every Tasmota loop iteration\n # it is optimized to be as fast as possible and reduce overhead\n # there is no introspect, closures must be registered directly\n def fast_loop()\n var fl = self._fl\n if !fl return end # fast exit if no closure is registered (most common case)\n\n # iterate and call each closure\n var i = 0\n var sz = size(fl)\n while i < sz\n # note: this is not guarded in try/except for performance reasons. The inner function must not raise exceptions\n fl[i]()\n i += 1\n end\n end\n\n # check that the parameter is not a method, it would require a closure instead\n def check_not_method(f)\n import introspect\n if type(f) != 'function'\n raise \"type_error\", \"BRY: argument must be a function\"\n end\n if introspect.ismethod(f) == true\n raise \"type_error\", \"BRY: method not allowed, use a closure like '/ args -> obj.func(args)'\"\n end\n end\n\n def add_fast_loop(cl)\n self.check_not_method(cl)\n if self._fl == nil\n self._fl = []\n end\n if type(cl) != 'function' raise \"value_error\", \"argument must be a function\" end\n global.fast_loop_enabled = 1 # enable fast_loop at global level: `TasmotaGlobal.fast_loop_enabled = true`\n self._fl.push(cl)\n end\n\n def remove_fast_loop(cl)\n if !self._fl return end\n var idx = self._fl.find(cl)\n if idx != nil\n self._fl.remove(idx)\n end\n end\n\n def sine_int(i)\n import math\n\n var x = i / 16384.0 * math.pi\n var y = math.sin(x)\n var r = int(y * 4096)\n return r\n end\n\n def delay(t)\n # do nothing in this simulation environment\n end\n\nend\n\nimport light_state\nimport Leds\nimport Leds_frame\n\nglobal.tasmota = Tasmota()\n\nif global.contains(\"__JS__\")\n global._fast_loop = def ()\n tasmota.fast_loop()\n end\nend\n\nreturn tasmota\n"; modules["user_functions.be"] = "# User-defined functions for Animation DSL\n# This file demonstrates how to create custom functions that can be used in the DSL\n\n# Example 1: provide a random value in range 0..255\ndef rand_demo(engine)\n import math\n return math.rand() % 256\nend\n\n# Factory function for rainbow palette\n#\n# @param engine: AnimationEngine - Animation engine reference (required for user function signature)\n# @param num_colors: int - Number of colors in the rainbow (default: 6)\n# @return bytes - A palette object containing rainbow colors in VRGB format\ndef color_wheel_palette(engine, num_colors)\n # Default parameters\n if num_colors == nil || num_colors < 2\n num_colors = 6\n end\n \n # Create a rainbow palette as bytes object\n var palette = bytes()\n var i = 0\n while i < num_colors\n # Calculate hue (0 to 360 degrees)\n var hue = tasmota.scale_uint(i, 0, num_colors, 0, 360)\n \n # Convert HSV to RGB (simplified conversion)\n var r, g, b\n var h_section = (hue / 60) % 6\n var f = (hue / 60) - h_section\n var v = 255 # Value (brightness)\n var p = 0 # Saturation is 100%, so p = 0\n var q = int(v * (1 - f))\n var t = int(v * f)\n \n if h_section == 0\n r = v; g = t; b = p\n elif h_section == 1\n r = q; g = v; b = p\n elif h_section == 2\n r = p; g = v; b = t\n elif h_section == 3\n r = p; g = q; b = v\n elif h_section == 4\n r = t; g = p; b = v\n else\n r = v; g = p; b = q\n end\n \n # Create ARGB color (fully opaque) and add to palette\n var color = (255 << 24) | (r << 16) | (g << 8) | b\n palette.add(color, -4) # Add as 4-byte big-endian\n i += 1\n end\n \n return palette\nend\n\n# Register all user functions with the animation module\nanimation.register_user_function(\"rand_demo\", rand_demo)\nanimation.register_user_function(\"color_wheel_palette\", color_wheel_palette)\n"; modules["webui/animation_web_ui.be"] = "#\n# berry_animation_webui.be - Web interface for Berry Animation Framework\n#\n# Provides a web-based DSL editor with live preview and code generation\n# Integrates with existing Tasmota web infrastructure for memory efficiency\n#\n# Copyright (C) 2024 Tasmota Project\n#\n\nclass AnimationWebUI\n var last_dsl_code\n var last_berry_code\n \n static var DEFAULT_DSL = \n \"# Simple Berry Animation Example - Cylon red eye\\n\"\n \"\\n\"\n \"set strip_len = strip_length()\\n\"\n \"\\n\"\n \"animation red_eye = beacon(\\n\"\n \" color = red\\n\"\n \" pos = smooth(min_value = 0, max_value = strip_len - 2, duration = 5s)\\n\"\n \" beacon_size = 3 # small 3 pixels eye\\n\"\n \" slew_size = 2 # with 2 pixel shading around\\n\"\n \")\\n\"\n \"\\n\"\n \"run red_eye # run the animation\\n\"\n\n def init()\n self.last_dsl_code = self.DEFAULT_DSL\n self.last_berry_code = \"\"\n \n # Add to main menu if not already present\n tasmota.add_driver(self)\n if tasmota.is_network_up()\n self.web_add_handler() # if init is called after the network is up, `web_add_handler` event is not fired\n end\n \n log(\"LED: Berry Animation WebUI initialized\", 3)\n end\n\n #####################################################################################################\n # Web handlers\n #####################################################################################################\n # Displays a \"Extension Manager\" button on the configuration page\n def web_add_button()\n import webserver\n webserver.content_send(\"

\")\n end\n\n def handle_request()\n import webserver\n import animation_dsl\n \n # API requests (JSON responses)\n if webserver.has_arg(\"api\")\n var api_type = webserver.arg(\"api\")\n\n if api_type == \"action\"\n # Action API (JSON response)\n webserver.content_open(200, \"application/json\")\n var result = {}\n \n if webserver.has_arg(\"action\")\n var action = webserver.arg(\"action\")\n \n if action == \"compile\" || action == \"compile_only\"\n if webserver.has_arg(\"dsl_code\")\n self.last_dsl_code = webserver.arg(\"dsl_code\")\n \n try\n # Compile DSL to Berry code\n self.last_berry_code = animation_dsl.compile(self.last_dsl_code)\n result[\"success\"] = true\n result[\"berry_code\"] = self.last_berry_code\n \n if action == \"compile\"\n # Execute the animation\n animation_dsl.execute(self.last_dsl_code)\n result[\"message\"] = \"Animation compiled and started\"\n else\n result[\"message\"] = \"DSL compiled successfully\"\n end\n \n except .. as e, msg\n result[\"success\"] = false\n result[\"error\"] = f\"{e}: {msg}\"\n self.last_berry_code = f\"# Compilation failed\\n# {result['error']}\"\n end\n else\n result[\"success\"] = false\n result[\"error\"] = \"No DSL code provided\"\n end\n \n elif action == \"stop\"\n animation.init_strip()\n result[\"success\"] = true\n result[\"message\"] = \"Animation stopped\"\n else\n result[\"success\"] = false\n result[\"error\"] = f\"Unknown action: {action}\"\n end\n else\n result[\"success\"] = false\n result[\"error\"] = \"No action specified\"\n end\n \n import json\n webserver.content_send(json.dump(result))\n webserver.content_close()\n end\n else \n # Default: serve main page (GET request)\n self.page_main()\n end\n end\n\n def page_main()\n import webserver\n webserver.content_start(\"Berry Animation Framework\")\n webserver.content_send_style()\n \n # Add custom CSS for the animation editor\n webserver.content_send(\n \"\"\n )\n\n webserver.content_send(\n \"
\"\n \n # DSL Editor\n \"

DSL Code Editor

\"\n \"
\"\n \"\"\n \"
\"\n \"
\"\n \"
\"\n \"
Status: Ready
\"\n \"

\"\n \"

\"\n \"

\"\n \"\"\n )\n\n # Generated Berry Code Display\n webserver.content_send(\n \"

Generated Berry Code

\"\n \"
\"\n \"\"\n \"
\"\n \"
\"\n \"
\"\n )\n\n # Add button at the end of the page\n webserver.content_button(webserver.BUTTON_MANAGEMENT)\n\n # Add JavaScript for AJAX\n webserver.content_send(\n \"\"\n )\n\n webserver.content_stop()\n end\n\n\n # Add HTTP POST and GET handlers\n def web_add_handler()\n import webserver\n webserver.on(\"/berry_anim\", / -> self.handle_request())\n end\n \n def deinit()\n # Cleanup when module is unloaded\n log(\"LED: Berry Animation WebUI deinitialized\", 3)\n end\nend\n\nreturn {\n \"animation_web_ui\": AnimationWebUI\n}\n"; // Register all modules in the virtual filesystem let count = 0; for (const [name, content] of Object.entries(modules)) { window.virtualFS.registerFile(name, content); count++; } console.log(`[BerryModules] Registered ${count} Berry modules`); // Export module list for debugging window.berryModuleList = Object.keys(modules); })();