Berry animation improvements (#24162)
This commit is contained in:
parent
07a1a982cd
commit
ec32b83f75
@ -377,6 +377,26 @@ SUCCESS
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## demo_value_meter.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
|
||||
## Symbol Table
|
||||
|
||||
| Symbol | Type | Builtin | Dangerous | Takes Args |
|
||||
|---------------------------|----------------------------|---------|-----------|------------|
|
||||
| `back_pattern` | animation | | | |
|
||||
| `closure_value` | value_provider_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `palette_meter_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `rainbow_with_white` | palette | | | |
|
||||
| `rand_meter` | user_function | | | ✓ |
|
||||
|
||||
### Compilation Output
|
||||
|
||||
```
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## disco_strobe.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
@ -702,34 +722,6 @@ SUCCESS
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## plasma_wave.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
|
||||
## Symbol Table
|
||||
|
||||
| Symbol | Type | Builtin | Dangerous | Takes Args |
|
||||
|--------------------------|----------------------------|---------|-----------|------------|
|
||||
| `SINE` | constant | ✓ | | |
|
||||
| `beacon_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `plasma_base` | animation | | | |
|
||||
| `plasma_colors` | palette | | | |
|
||||
| `plasma_wave1` | animation | | | |
|
||||
| `plasma_wave2` | animation | | | |
|
||||
| `plasma_wave3` | animation | | | |
|
||||
| `rich_palette_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `rich_palette` | color_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `smooth` | value_provider_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `wave1_pattern` | color | | | |
|
||||
| `wave2_pattern` | color | | | |
|
||||
| `wave3_pattern` | color | | | |
|
||||
|
||||
### Compilation Output
|
||||
|
||||
```
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## palette_demo.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
@ -789,6 +781,34 @@ SUCCESS
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## plasma_wave.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
|
||||
## Symbol Table
|
||||
|
||||
| Symbol | Type | Builtin | Dangerous | Takes Args |
|
||||
|--------------------------|----------------------------|---------|-----------|------------|
|
||||
| `SINE` | constant | ✓ | | |
|
||||
| `beacon_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `plasma_base` | animation | | | |
|
||||
| `plasma_colors` | palette | | | |
|
||||
| `plasma_wave1` | animation | | | |
|
||||
| `plasma_wave2` | animation | | | |
|
||||
| `plasma_wave3` | animation | | | |
|
||||
| `rich_palette_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `rich_palette` | color_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `smooth` | value_provider_constructor | ✓ | ⚠️ | ✓ |
|
||||
| `wave1_pattern` | color | | | |
|
||||
| `wave2_pattern` | color | | | |
|
||||
| `wave3_pattern` | color | | | |
|
||||
|
||||
### Compilation Output
|
||||
|
||||
```
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
## police_lights.anim
|
||||
|
||||
**Status:** ✅ Success
|
||||
@ -1236,8 +1256,8 @@ SUCCESS
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total files processed:** 50
|
||||
- **Successfully compiled:** 47
|
||||
- **Total files processed:** 51
|
||||
- **Successfully compiled:** 48
|
||||
- **Failed to compile:** 3
|
||||
|
||||
### Successful Files
|
||||
@ -1257,6 +1277,7 @@ SUCCESS
|
||||
- ✅ demo_shutter_rainbow_central.anim
|
||||
- ✅ demo_shutter_rainbow_leftright.anim
|
||||
- ✅ demo_shutter_rainbow2.anim
|
||||
- ✅ demo_value_meter.anim
|
||||
- ✅ disco_strobe.anim
|
||||
- ✅ fire_flicker.anim
|
||||
- ✅ heartbeat_pulse.anim
|
||||
@ -1267,9 +1288,9 @@ SUCCESS
|
||||
- ✅ meteor_shower.anim
|
||||
- ✅ neon_glow.anim
|
||||
- ✅ ocean_waves.anim
|
||||
- ✅ plasma_wave.anim
|
||||
- ✅ palette_demo.anim
|
||||
- ✅ palette_showcase.anim
|
||||
- ✅ plasma_wave.anim
|
||||
- ✅ police_lights.anim
|
||||
- ✅ property_assignment_demo.anim
|
||||
- ✅ rainbow_cycle.anim
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
# Generated Berry code from Animation DSL
|
||||
# Source: demo_value_meter.anim
|
||||
#
|
||||
# This file was automatically generated by compile_all_examples.sh
|
||||
# Do not edit manually - changes will be overwritten
|
||||
|
||||
import animation
|
||||
|
||||
# Pattern of colors in the background based on palette, rotating over 5 s
|
||||
# Auto-generated strip initialization (using Tasmota configuration)
|
||||
var engine = animation.init_strip()
|
||||
|
||||
# Berry code block
|
||||
|
||||
def rand_meter(time_ms, self)
|
||||
import math
|
||||
var r = math.rand() % 101
|
||||
return r
|
||||
end
|
||||
|
||||
# End berry code block
|
||||
# External function declaration: rand_meter
|
||||
animation.register_user_function("rand_meter", rand_meter)
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
var rainbow_with_white_ = bytes(
|
||||
"FFFC0000" # Red
|
||||
"FFFF8000" # Orange
|
||||
"FFFFFF00" # Yellow
|
||||
"FF00FF00" # Green
|
||||
"FF00FFFF" # Cyan
|
||||
"FF0080FF" # Blue
|
||||
"FF8000FF" # Violet
|
||||
"FFCCCCCC" # White
|
||||
"FFFC0000" # Red - need to add the first color at last position to ensure roll-over
|
||||
)
|
||||
# define a gradient across the whole strip
|
||||
var back_pattern_ = animation.palette_meter_animation(engine)
|
||||
back_pattern_.value_func = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_meter')(engine) end)
|
||||
engine.add(back_pattern_)
|
||||
engine.run()
|
||||
|
||||
|
||||
#- Original DSL source:
|
||||
# Pattern of colors in the background based on palette, rotating over 5 s
|
||||
|
||||
berry """
|
||||
def rand_meter(time_ms, self)
|
||||
import math
|
||||
var r = math.rand() % 101
|
||||
return r
|
||||
end
|
||||
"""
|
||||
|
||||
extern function rand_meter
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_meter_animation(value_func = rand_meter)
|
||||
|
||||
run back_pattern
|
||||
|
||||
-#
|
||||
@ -0,0 +1,29 @@
|
||||
# Pattern of colors in the background based on palette, rotating over 5 s
|
||||
|
||||
berry """
|
||||
def rand_meter(time_ms, self)
|
||||
import math
|
||||
var r = math.rand() % 101
|
||||
return r
|
||||
end
|
||||
"""
|
||||
|
||||
extern function rand_meter
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
0xFF8000 # Orange
|
||||
0xFFFF00 # Yellow
|
||||
0x00FF00 # Green
|
||||
0x00FFFF # Cyan
|
||||
0x0080FF # Blue
|
||||
0x8000FF # Violet
|
||||
0xCCCCCC # White
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_meter_animation(value_func = rand_meter)
|
||||
|
||||
run back_pattern
|
||||
@ -13,7 +13,7 @@ palette rainbow_with_white = [
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a color attribute that cycles over time, cycle is 10 seconds
|
||||
# define a color attribute cycles color in space
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
|
||||
|
||||
# define a gradient across the whole strip
|
||||
|
||||
@ -8,6 +8,8 @@ def rand_meter(time_ms, self)
|
||||
end
|
||||
"""
|
||||
|
||||
extern function rand_meter
|
||||
|
||||
# define a palette of rainbow colors including white with constant brightness
|
||||
palette rainbow_with_white = [
|
||||
0xFC0000 # Red
|
||||
@ -21,7 +23,10 @@ palette rainbow_with_white = [
|
||||
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
|
||||
]
|
||||
|
||||
# define a color attribute cycles color in space
|
||||
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
|
||||
|
||||
# define a gradient across the whole strip
|
||||
animation back_pattern = palette_meter_animation(value_func = rand_meter)
|
||||
animation back_pattern = palette_meter_animation(color_source = rainbow_rich_color, value_func = rand_meter)
|
||||
|
||||
run back_pattern
|
||||
|
||||
@ -22,7 +22,8 @@ ParameterizedObject (base class with parameter management and playable interface
|
||||
│ ├── BeaconAnimation (pulse at specific position)
|
||||
│ ├── CrenelPositionAnimation (crenel/square wave pattern)
|
||||
│ ├── BreatheAnimation (breathing effect)
|
||||
│ ├── PalettePatternAnimation (base for palette-based animations)
|
||||
│ ├── PaletteGradientAnimation (gradient patterns with palette colors)
|
||||
│ │ └── PaletteMeterAnimation (meter/bar patterns)
|
||||
│ ├── CometAnimation (moving comet with tail)
|
||||
│ ├── FireAnimation (realistic fire effect)
|
||||
│ ├── TwinkleAnimation (twinkling stars effect)
|
||||
@ -243,11 +244,11 @@ Generates oscillating values using various waveforms. Inherits from `ValueProvid
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `min_value` | int | 0 | - | Minimum oscillation value |
|
||||
| `max_value` | int | 100 | - | Maximum oscillation value |
|
||||
| `max_value` | int | 255 | - | Maximum oscillation value |
|
||||
| `duration` | int | 1000 | min: 1 | Oscillation period in milliseconds |
|
||||
| `form` | int | 1 | enum: [1,2,3,4,5,6,7,8,9] | Waveform type |
|
||||
| `phase` | int | 0 | 0-100 | Phase shift percentage |
|
||||
| `duty_cycle` | int | 50 | 0-100 | Duty cycle for square/triangle waves |
|
||||
| `phase` | int | 0 | 0-255 | Phase shift in 0-255 range (mapped to duration) |
|
||||
| `duty_cycle` | int | 127 | 0-255 | Duty cycle for square/triangle waves in 0-255 range |
|
||||
|
||||
**Waveform Constants**:
|
||||
- `1` (SAWTOOTH) - Linear ramp from min to max
|
||||
@ -301,6 +302,18 @@ The ClosureValueProvider includes built-in mathematical helper methods that can
|
||||
- **Cosine Behavior**: Matches oscillator COSINE waveform (starts at minimum, not maximum)
|
||||
- **Scale Function**: Uses `tasmota.scale_int()` for efficient integer scaling
|
||||
|
||||
#### Closure Signature
|
||||
|
||||
Closures used with ClosureValueProvider must follow this signature:
|
||||
```berry
|
||||
def closure_func(engine, param_name, time_ms)
|
||||
# engine: AnimationEngine reference
|
||||
# param_name: Name of the parameter being computed
|
||||
# time_ms: Current time in milliseconds
|
||||
return computed_value
|
||||
end
|
||||
```
|
||||
|
||||
#### Usage in Computed Values
|
||||
|
||||
These methods are automatically available in DSL computed expressions:
|
||||
@ -1029,51 +1042,23 @@ animation strobe = wave_animation(
|
||||
|
||||
|
||||
|
||||
### PalettePatternAnimation
|
||||
### PaletteGradientAnimation
|
||||
|
||||
Applies colors from a color provider to specific patterns using an efficient bytes() buffer. Inherits from `Animation`.
|
||||
Creates shifting gradient patterns with palette colors. Inherits from `Animation`.
|
||||
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `color_source` | instance | nil | - | Color provider for pattern mapping |
|
||||
| `pattern_func` | function | nil | - | Function that generates pattern values (0-255) for each pixel |
|
||||
| `shift_period` | int | 0 | min: 0 | Time for one complete shift cycle in ms (0 = static gradient) |
|
||||
| `spatial_period` | int | 0 | min: 0 | Spatial period in pixels (0 = full strip length) |
|
||||
| `phase_shift` | int | 0 | 0-255 | Phase shift in 0-255 range (mapped to spatial period) |
|
||||
| *(inherits all Animation parameters)* | | | | |
|
||||
|
||||
**Implementation Details:**
|
||||
- Uses `bytes()` buffer for efficient storage of per-pixel values
|
||||
- Pattern function should return values in 0-255 range
|
||||
- Color source receives values in 0-255 range via `get_color_for_value(value, time_ms)`
|
||||
- Buffer automatically resizes when strip length changes
|
||||
|
||||
**Factory**: `animation.palette_pattern_animation(engine)`
|
||||
|
||||
### PaletteWaveAnimation
|
||||
|
||||
Creates sine wave patterns with palette colors. Inherits from `PalettePatternAnimation`.
|
||||
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `wave_period` | int | 5000 | min: 1 | Wave animation period in ms |
|
||||
| `wave_length` | int | 10 | min: 1 | Wave length in pixels |
|
||||
| *(inherits all PalettePatternAnimation parameters)* | | | | |
|
||||
|
||||
**Pattern Generation:**
|
||||
- Generates sine wave values in 0-255 range using `tasmota.sine_int()`
|
||||
- Wave position advances based on `wave_period` timing
|
||||
- Each pixel's value calculated as: `sine_value = tasmota.scale_int(sine_int(angle), -4096, 4096, 0, 255)`
|
||||
|
||||
**Factory**: `animation.palette_wave_animation(engine)`
|
||||
|
||||
### PaletteGradientAnimation
|
||||
|
||||
Creates shifting gradient patterns with palette colors. Inherits from `PalettePatternAnimation`.
|
||||
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `shift_period` | int | 10000 | min: 0 | Time for one complete shift cycle in ms (0 = static gradient) |
|
||||
| `spatial_period` | int | 0 | min: 0 | Spatial period in pixels (0 = full strip length) |
|
||||
| `phase_shift` | int | 0 | 0-100 | Phase shift as percentage of spatial period |
|
||||
| *(inherits all PalettePatternAnimation parameters)* | | | | |
|
||||
- Optimized LUT (Lookup Table) support for color providers
|
||||
|
||||
**Pattern Generation:**
|
||||
- Generates linear gradient values in 0-255 range across the specified spatial period
|
||||
@ -1084,23 +1069,27 @@ Creates shifting gradient patterns with palette colors. Inherits from `PalettePa
|
||||
- `0`: Gradient spans the full strip length (single gradient across entire strip)
|
||||
- `> 0`: Gradient repeats every N pixels
|
||||
- **phase_shift**: Shifts the gradient pattern spatially by a percentage of the spatial period
|
||||
- Each pixel's value calculated as: `value = tasmota.scale_uint(spatial_position, 0, spatial_period-1, 0, 255)`
|
||||
- Each pixel's value calculated using optimized fixed-point arithmetic
|
||||
|
||||
**Factory**: `animation.palette_gradient_animation(engine)`
|
||||
|
||||
### PaletteMeterAnimation
|
||||
|
||||
Creates meter/bar patterns based on a value function. Inherits from `PalettePatternAnimation`.
|
||||
Creates meter/bar patterns based on a value function. Inherits from `PaletteGradientAnimation`.
|
||||
|
||||
| Parameter | Type | Default | Constraints | Description |
|
||||
|-----------|------|---------|-------------|-------------|
|
||||
| `value_func` | function | nil | - | Function that provides meter values (0-100 range) |
|
||||
| *(inherits all PalettePatternAnimation parameters)* | | | | |
|
||||
| `value_func` | function | nil | - | Function that provides meter values (0-255 range) |
|
||||
| *(inherits all PaletteGradientAnimation parameters)* | | | | |
|
||||
|
||||
**Pattern Generation:**
|
||||
- Value function returns percentage (0-100) representing meter level
|
||||
- Value function signature: `value_func(engine, time_ms, self)` where:
|
||||
- `engine`: AnimationEngine reference
|
||||
- `time_ms`: Elapsed time since animation start
|
||||
- `self`: Reference to the animation instance
|
||||
- Value function returns value in 0-255 range representing meter level
|
||||
- Pixels within meter range get value 255, others get value 0
|
||||
- Meter position calculated as: `position = tasmota.scale_uint(value, 0, 100, 0, strip_length)`
|
||||
- Meter position calculated as: `position = tasmota.scale_uint(value, 0, 255, 0, strip_length)`
|
||||
|
||||
**Factory**: `animation.palette_meter_animation(engine)`
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ The following keywords are reserved and cannot be used as identifiers:
|
||||
- `set` - Variable assignment
|
||||
- `import` - Import Berry modules
|
||||
- `berry` - Embed arbitrary Berry code
|
||||
- `extern` - Declare external Berry functions for DSL use
|
||||
|
||||
**Definition Keywords:**
|
||||
- `color` - Color definition
|
||||
@ -295,6 +296,70 @@ print("Animation configured")
|
||||
run pulse
|
||||
```
|
||||
|
||||
### External Function Declarations
|
||||
|
||||
The `extern` keyword declares Berry functions defined in `berry` code blocks so they can be used in DSL expressions and computed parameters:
|
||||
|
||||
```berry
|
||||
# Define the function in a berry block
|
||||
berry """
|
||||
def rand_meter(time_ms, self)
|
||||
import math
|
||||
var r = math.rand() % 101
|
||||
return r
|
||||
end
|
||||
|
||||
def breathing_effect(base_value, amplitude)
|
||||
import math
|
||||
var time_factor = (tasmota.millis() / 1000) % 4 # 4-second cycle
|
||||
var breath = math.sin(time_factor * math.pi / 2)
|
||||
return int(base_value + breath * amplitude)
|
||||
end
|
||||
"""
|
||||
|
||||
# Declare functions as external so DSL knows about them
|
||||
extern function rand_meter
|
||||
extern function breathing_effect
|
||||
|
||||
# Now they can be used in DSL expressions
|
||||
animation back_pattern = palette_meter_animation(value_func = rand_meter)
|
||||
animation breathing_light = solid(color=blue)
|
||||
breathing_light.opacity = breathing_effect
|
||||
|
||||
run back_pattern
|
||||
```
|
||||
|
||||
**External Function Features:**
|
||||
- Functions must be declared with `extern function function_name` after being defined in `berry` blocks
|
||||
- Can be used with or without parentheses: `rand_meter` or `rand_meter()`
|
||||
- Automatically receive `engine` as the first parameter when called from DSL
|
||||
- Can be used in computed parameters and property assignments
|
||||
- Support the same signature pattern as registered user functions
|
||||
|
||||
**Function Signature Requirements:**
|
||||
External functions should follow the user function pattern where the first parameter is the engine:
|
||||
|
||||
```berry
|
||||
berry """
|
||||
def my_external_func(engine, param1, param2)
|
||||
# engine is automatically provided by DSL
|
||||
# param1, param2 are user-provided parameters
|
||||
return computed_value
|
||||
end
|
||||
"""
|
||||
|
||||
extern function my_external_func
|
||||
|
||||
# Usage in DSL (engine parameter is automatic)
|
||||
animation test = solid(color=red)
|
||||
test.opacity = my_external_func # Called as my_external_func(engine)
|
||||
```
|
||||
|
||||
**Comparison with User Functions:**
|
||||
- **External functions**: Defined in `berry` blocks within the same DSL file, declared with `extern function`
|
||||
- **User functions**: Defined in separate Berry modules, imported with `import`, registered with `animation.register_user_function()`
|
||||
- Both use the same calling convention and can be used interchangeably in DSL expressions
|
||||
|
||||
## Color Definitions
|
||||
|
||||
The `color` keyword defines static colors or color providers:
|
||||
@ -1449,11 +1514,13 @@ statement = import_stmt
|
||||
| property_assignment
|
||||
| sequence
|
||||
| template_def
|
||||
| external_stmt
|
||||
| execution_stmt ;
|
||||
|
||||
(* Import and Configuration *)
|
||||
import_stmt = "import" identifier ;
|
||||
config_stmt = variable_assignment ;
|
||||
external_stmt = "extern" "function" identifier ;
|
||||
(* strip_config = "strip" "length" number ; -- TEMPORARILY DISABLED *)
|
||||
variable_assignment = "set" identifier "=" expression ;
|
||||
|
||||
@ -1607,6 +1674,7 @@ This applies to:
|
||||
- Execution statements
|
||||
- **Template animations**: Reusable animation classes with parameters extending `engine_proxy`
|
||||
- User-defined functions (with engine-first parameter pattern) - see **[User Functions Guide](USER_FUNCTIONS.md)**
|
||||
- **External function declarations**: `extern` keyword to declare Berry functions defined in `berry` blocks for DSL use
|
||||
- **User functions in computed parameters**: User functions can be used in arithmetic expressions alongside mathematical functions
|
||||
- **Flexible parameter syntax**: Commas optional when parameters are on separate lines
|
||||
- **Computed values**: Arithmetic expressions with value providers automatically create closures
|
||||
|
||||
@ -31,7 +31,7 @@ class GradientAnimation : animation.animation
|
||||
self.phase_offset = 0
|
||||
|
||||
# Initialize with default strip length from engine
|
||||
var strip_length = self.engine.get_strip_length()
|
||||
var strip_length = self.engine.strip_length
|
||||
self.current_colors.resize(strip_length)
|
||||
|
||||
# Initialize colors to black
|
||||
@ -47,7 +47,7 @@ class GradientAnimation : animation.animation
|
||||
super(self).on_param_changed(name, value)
|
||||
# TODO maybe be more specific on attribute name
|
||||
# Handle strip length changes from engine
|
||||
var current_strip_length = self.engine.get_strip_length()
|
||||
var current_strip_length = self.engine.strip_length
|
||||
if size(self.current_colors) != current_strip_length
|
||||
self.current_colors.resize(current_strip_length)
|
||||
var i = size(self.current_colors)
|
||||
@ -92,7 +92,7 @@ class GradientAnimation : animation.animation
|
||||
# Cache parameter values for performance
|
||||
var gradient_type = self.gradient_type
|
||||
var color_param = self.color
|
||||
var strip_length = self.engine.get_strip_length()
|
||||
var strip_length = self.engine.strip_length
|
||||
|
||||
# Ensure current_colors array matches strip length
|
||||
if size(self.current_colors) != strip_length
|
||||
@ -205,7 +205,7 @@ class GradientAnimation : animation.animation
|
||||
# Auto-fix time_ms and start_time
|
||||
time_ms = self._fix_time_ms(time_ms)
|
||||
|
||||
var strip_length = self.engine.get_strip_length()
|
||||
var strip_length = self.engine.strip_length
|
||||
var i = 0
|
||||
while i < strip_length && i < frame.width
|
||||
if i < size(self.current_colors)
|
||||
@ -271,4 +271,7 @@ def gradient_two_color_linear(engine)
|
||||
return anim
|
||||
end
|
||||
|
||||
return {'gradient_animation': GradientAnimation, 'gradient_rainbow_linear': gradient_rainbow_linear, 'gradient_rainbow_radial': gradient_rainbow_radial, 'gradient_two_color_linear': gradient_two_color_linear}
|
||||
return {'gradient_animation': GradientAnimation,
|
||||
'gradient_rainbow_linear': gradient_rainbow_linear,
|
||||
'gradient_rainbow_radial': gradient_rainbow_radial,
|
||||
'gradient_two_color_linear': gradient_two_color_linear}
|
||||
@ -1,25 +1,25 @@
|
||||
# PalettePattern animation effect for Berry Animation Framework
|
||||
# PaletteGradient animation effect for Berry Animation Framework
|
||||
#
|
||||
# This animation applies colors from a color provider to specific patterns or regions.
|
||||
# It allows for more complex visual effects by combining palette colors with patterns.
|
||||
#
|
||||
# This version supports both RichPaletteAnimation and ColorProvider instances as color sources,
|
||||
# allowing for more flexible usage of color providers.
|
||||
# This animation creates gradient patterns with palette colors.
|
||||
# It supports shifting gradients, spatial periods, and phase shifts.
|
||||
|
||||
import "./core/param_encoder" as encode_constraints
|
||||
|
||||
#@ solidify:PalettePatternAnimation,weak
|
||||
class PalettePatternAnimation : animation.animation
|
||||
# Gradient pattern animation - creates shifting gradient patterns
|
||||
#@ solidify:PaletteGradientAnimation,weak
|
||||
class PaletteGradientAnimation : animation.animation
|
||||
var value_buffer # Buffer to store values for each pixel (bytes object)
|
||||
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Palette pattern-specific parameters
|
||||
# Gradient-specific parameters
|
||||
"color_source": {"default": nil, "type": "instance"},
|
||||
"pattern_func": {"default": nil, "type": "function"}
|
||||
"shift_period": {"min": 0, "default": 0}, # Time for one complete shift cycle in ms (0 = static)
|
||||
"spatial_period": {"min": 0, "default": 0}, # Spatial period in pixels (0 = full strip)
|
||||
"phase_shift": {"min": 0, "max": 255, "default": 0} # Phase shift in 0-255 range
|
||||
})
|
||||
|
||||
# Initialize a new PalettePattern animation
|
||||
# Initialize a new gradient pattern animation
|
||||
#
|
||||
# @param engine: AnimationEngine - Required animation engine reference
|
||||
def init(engine)
|
||||
@ -29,6 +29,9 @@ class PalettePatternAnimation : animation.animation
|
||||
# Initialize non-parameter instance variables only
|
||||
self.value_buffer = bytes()
|
||||
|
||||
# Set default name
|
||||
self.name = "palette_gradient"
|
||||
|
||||
# Initialize value buffer with default frame width
|
||||
self._initialize_value_buffer()
|
||||
end
|
||||
@ -46,24 +49,47 @@ class PalettePatternAnimation : animation.animation
|
||||
end
|
||||
end
|
||||
|
||||
# Update the value buffer based on the current time
|
||||
#
|
||||
# @param time_ms: int - Current time in milliseconds
|
||||
# Update the value buffer to generate gradient pattern
|
||||
def _update_value_buffer(time_ms, strip_length)
|
||||
var pattern_func = self.pattern_func
|
||||
if pattern_func == nil
|
||||
return
|
||||
# Cache parameter values for performance
|
||||
var shift_period = self.shift_period
|
||||
var spatial_period = self.spatial_period
|
||||
var phase_shift = self.phase_shift
|
||||
|
||||
# Determine effective spatial period (0 means full strip)
|
||||
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
|
||||
|
||||
# Calculate the temporal shift position (how much the pattern has moved over time)
|
||||
var temporal_offset = 0
|
||||
if shift_period > 0
|
||||
temporal_offset = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, effective_spatial_period)
|
||||
end
|
||||
|
||||
# Calculate the phase shift offset in pixels
|
||||
var phase_offset = tasmota.scale_uint(phase_shift, 0, 255, 0, effective_spatial_period)
|
||||
|
||||
# Calculate values for each pixel
|
||||
var i = 0
|
||||
# Calculate position within the spatial period, including temporal and phase offsets
|
||||
var spatial_pos = (temporal_offset + phase_offset) % effective_spatial_period
|
||||
|
||||
# Calculate the increment per pixel, in 1/1024 of pixels
|
||||
# We calculate 1024*255/effective_spatial_period
|
||||
# But for rounding we actually calculate
|
||||
# ((1024 * 255 * 2) + 1) / (2 * effective_spatial_period)
|
||||
# Note: (1024 * 255 * 2) + 1 = 522241
|
||||
var incr_1024 = (522241 / effective_spatial_period) >> 1
|
||||
|
||||
# 'spatial_1024' is our accumulator in 1/1024th of pixels, 2^10
|
||||
var spatial_1024 = spatial_pos * incr_1024
|
||||
var buffer = self.value_buffer._buffer() # 'buffer' is of type 'comptr'
|
||||
|
||||
# var effective_spatial_period_1 = effective_spatial_period - 1
|
||||
# # Calculate the increment in 1/256 of values
|
||||
# var increment = tasmota.scale_uint(effective_spatial_period)
|
||||
while i < strip_length
|
||||
var pattern_value = pattern_func(i, time_ms, self)
|
||||
# Pattern function should return values in 0-255 range, clamp to byte range
|
||||
var byte_value = int(pattern_value)
|
||||
if byte_value < 0 byte_value = 0 end
|
||||
if byte_value > 255 byte_value = 255 end
|
||||
self.value_buffer[i] = byte_value
|
||||
buffer[i] = spatial_1024 >> 10
|
||||
spatial_1024 += incr_1024 # we don't really care about overflow since we clamp modula 255 anyways
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
@ -163,8 +189,8 @@ class PalettePatternAnimation : animation.animation
|
||||
# Handle parameter changes
|
||||
def on_param_changed(name, value)
|
||||
super(self).on_param_changed(name, value)
|
||||
if name == "pattern_func" || name == "color_source"
|
||||
# Reinitialize value buffer when pattern or color source changes
|
||||
if name == "color_source"
|
||||
# Reinitialize value buffer when color source changes
|
||||
self._initialize_value_buffer()
|
||||
end
|
||||
end
|
||||
@ -176,125 +202,9 @@ class PalettePatternAnimation : animation.animation
|
||||
end
|
||||
end
|
||||
|
||||
# Wave pattern animation - creates sine wave patterns
|
||||
#@ solidify:PaletteWaveAnimation,weak
|
||||
class PaletteWaveAnimation : PalettePatternAnimation
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Wave-specific parameters only
|
||||
"wave_period": {"min": 1, "default": 5000},
|
||||
"wave_length": {"min": 1, "default": 10}
|
||||
})
|
||||
|
||||
# Initialize a new wave pattern animation
|
||||
#
|
||||
# @param engine: AnimationEngine - Required animation engine reference
|
||||
def init(engine)
|
||||
# Call parent constructor
|
||||
super(self).init(engine)
|
||||
|
||||
# Set default name
|
||||
self.name = "palette_wave"
|
||||
end
|
||||
|
||||
# Override _update_value_buffer to generate wave pattern directly
|
||||
def _update_value_buffer(time_ms, strip_length)
|
||||
# Cache parameter values for performance
|
||||
var wave_period = self.wave_period
|
||||
var wave_length = self.wave_length
|
||||
|
||||
# Calculate the wave position using scale_uint for better precision
|
||||
# var position = tasmota.scale_uint(time_ms % wave_period, 0, wave_period, 0, 1000) / 1000.0
|
||||
# var offset = int(position * wave_length)
|
||||
var offset = tasmota.scale_uint(time_ms % wave_period, 0, wave_period, 0, wave_length)
|
||||
|
||||
# Calculate values for each pixel
|
||||
var i = 0
|
||||
while i < strip_length
|
||||
# Calculate the wave value (0-255) using scale_uint
|
||||
var pos_in_wave = (i + offset) % wave_length
|
||||
var angle = tasmota.scale_uint(pos_in_wave, 0, wave_length, 0, 32767) # 0 to 2π in fixed-point
|
||||
var sine_value = tasmota.sine_int(angle) # -4096 to 4096
|
||||
|
||||
# Map sine value from -4096..4096 to 0..255
|
||||
var byte_value = tasmota.scale_int(sine_value, -4096, 4096, 0, 255)
|
||||
self.value_buffer[i] = byte_value
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Gradient pattern animation - creates shifting gradient patterns
|
||||
#@ solidify:PaletteGradientAnimation,weak
|
||||
class PaletteGradientAnimation : PalettePatternAnimation
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Gradient-specific parameters only
|
||||
"shift_period": {"min": 0, "default": 0}, # Time for one complete shift cycle in ms (0 = static)
|
||||
"spatial_period": {"min": 0, "default": 0}, # Spatial period in pixels (0 = full strip)
|
||||
"phase_shift": {"min": 0, "max": 100, "default": 0} # Phase shift as percentage (0-100)
|
||||
})
|
||||
|
||||
# Initialize a new gradient pattern animation
|
||||
#
|
||||
# @param engine: AnimationEngine - Required animation engine reference
|
||||
def init(engine)
|
||||
# Call parent constructor
|
||||
super(self).init(engine)
|
||||
|
||||
# Set default name
|
||||
self.name = "palette_gradient"
|
||||
end
|
||||
|
||||
# Override _update_value_buffer to generate gradient pattern directly
|
||||
def _update_value_buffer(time_ms, strip_length)
|
||||
# Cache parameter values for performance
|
||||
var shift_period = self.shift_period
|
||||
var spatial_period = self.spatial_period
|
||||
var phase_shift = self.phase_shift
|
||||
|
||||
# Determine effective spatial period (0 means full strip)
|
||||
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
|
||||
|
||||
# Calculate the temporal shift position (how much the pattern has moved over time)
|
||||
var temporal_offset = 0
|
||||
if shift_period > 0
|
||||
temporal_offset = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, effective_spatial_period)
|
||||
end
|
||||
|
||||
# Calculate the phase shift offset in pixels
|
||||
var phase_offset = tasmota.scale_uint(phase_shift, 0, 100, 0, effective_spatial_period)
|
||||
|
||||
# Calculate values for each pixel
|
||||
var i = 0
|
||||
# Calculate position within the spatial period, including temporal and phase offsets
|
||||
var spatial_pos = (temporal_offset + phase_offset) % effective_spatial_period
|
||||
|
||||
# Calculate the increment per pixel, in 1/1024 of pixels
|
||||
# We calculate 1024*255/effective_spatial_period
|
||||
# But for rounding we actually calculate
|
||||
# ((1024 * 255 * 2) + 1) / (2 * effective_spatial_period)
|
||||
# Note: (1024 * 255 * 2) + 1 = 522241
|
||||
var incr_1024 = (522241 / effective_spatial_period) >> 1
|
||||
|
||||
# 'spatial_1024' is our accumulator in 1/1024th of pixels, 2^10
|
||||
var spatial_1024 = spatial_pos * incr_1024
|
||||
var buffer = self.value_buffer._buffer() # 'buffer' is of type 'comptr'
|
||||
|
||||
# var effective_spatial_period_1 = effective_spatial_period - 1
|
||||
# # Calculate the increment in 1/256 of values
|
||||
# var increment = tasmota.scale_uint(effective_spatial_period)
|
||||
while i < strip_length
|
||||
buffer[i] = spatial_1024 >> 10
|
||||
spatial_1024 += incr_1024 # we don't really care about overflow since we clamp modula 255 anyways
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Value meter pattern animation - creates meter/bar patterns based on a value function
|
||||
#@ solidify:PaletteMeterAnimation,weak
|
||||
class PaletteMeterAnimation : PalettePatternAnimation
|
||||
class PaletteMeterAnimation : PaletteGradientAnimation
|
||||
# Static definitions of parameters with constraints
|
||||
static var PARAMS = animation.enc_params({
|
||||
# Meter-specific parameters only
|
||||
@ -320,11 +230,14 @@ class PaletteMeterAnimation : PalettePatternAnimation
|
||||
return
|
||||
end
|
||||
|
||||
# Cache engine reference to avoid dereferencing
|
||||
var engine = self.engine
|
||||
|
||||
# Get the current value
|
||||
var current_value = value_func(time_ms, self)
|
||||
var current_value = value_func(engine, time_ms, self)
|
||||
|
||||
# Calculate the meter position using scale_uint for better precision
|
||||
var meter_position = tasmota.scale_uint(current_value, 0, 100, 0, strip_length)
|
||||
var meter_position = tasmota.scale_uint(current_value, 0, 255, 0, strip_length)
|
||||
|
||||
# Calculate values for each pixel
|
||||
var i = 0
|
||||
@ -337,8 +250,6 @@ class PaletteMeterAnimation : PalettePatternAnimation
|
||||
end
|
||||
|
||||
return {
|
||||
'palette_pattern_animation': PalettePatternAnimation,
|
||||
'palette_wave_animation': PaletteWaveAnimation,
|
||||
'palette_gradient_animation': PaletteGradientAnimation,
|
||||
'palette_meter_animation': PaletteMeterAnimation
|
||||
}
|
||||
@ -587,17 +587,17 @@ extern "C" {
|
||||
|
||||
// Handle negative indices (Python-style)
|
||||
if (start_pos < 0) { start_pos += width; }
|
||||
if (end_pos < 0) { end_pos += width; }
|
||||
if (end_pos < 0) { end_pos += width + 1; }
|
||||
|
||||
// Clamp to valid range
|
||||
if (start_pos < 0) { start_pos = 0; }
|
||||
if (end_pos < 0) { end_pos = 0; }
|
||||
if (start_pos >= width) { be_return_nil(vm); }
|
||||
if (end_pos >= width) { end_pos = width - 1; }
|
||||
if (end_pos < start_pos) { be_return_nil(vm); }
|
||||
if (end_pos >= width) { end_pos = width; }
|
||||
if (end_pos <= start_pos) { be_return_nil(vm); }
|
||||
|
||||
// Fill the region with the color
|
||||
for (int32_t i = start_pos; i <= end_pos; i++) {
|
||||
for (int32_t i = start_pos; i < end_pos; i++) {
|
||||
pixels_buf[i] = color;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
# child management and rendering to the root animation.
|
||||
|
||||
class AnimationEngine
|
||||
# Minimum milliseconds between ticks
|
||||
static var TICK_MS = 50
|
||||
|
||||
# Core properties
|
||||
var strip # LED strip object
|
||||
var strip_length # Strip length (cached for performance)
|
||||
@ -17,6 +20,7 @@ class AnimationEngine
|
||||
var last_update # Last update time in milliseconds
|
||||
var time_ms # Current time in milliseconds (updated each frame)
|
||||
var fast_loop_closure # Stored closure for fast_loop registration
|
||||
var tick_ms # Minimum milliseconds between ticks (runtime configurable)
|
||||
|
||||
# Performance optimization
|
||||
var render_needed # Whether a render pass is needed
|
||||
@ -77,6 +81,7 @@ class AnimationEngine
|
||||
self.last_update = 0
|
||||
self.time_ms = 0
|
||||
self.fast_loop_closure = nil
|
||||
self.tick_ms = self.TICK_MS # Initialize from static default
|
||||
self.render_needed = false
|
||||
|
||||
# Initialize CPU metrics
|
||||
@ -187,25 +192,25 @@ class AnimationEngine
|
||||
return false
|
||||
end
|
||||
|
||||
# Start timing this tick
|
||||
self.ts_start = tasmota.millis()
|
||||
|
||||
if current_time == nil
|
||||
current_time = self.ts_start
|
||||
current_time = tasmota.millis()
|
||||
end
|
||||
|
||||
# Throttle updates based on tick_ms setting
|
||||
var delta_time = current_time - self.last_update
|
||||
if delta_time < self.tick_ms
|
||||
return true
|
||||
end
|
||||
|
||||
# Start timing this tick (use tasmota.millis() for consistent profiling)
|
||||
self.ts_start = tasmota.millis()
|
||||
|
||||
# Check if strip length changed since last time
|
||||
self.check_strip_length()
|
||||
|
||||
# Update engine time
|
||||
self.time_ms = current_time
|
||||
|
||||
# Throttle updates to ~5ms intervals
|
||||
var delta_time = current_time - self.last_update
|
||||
if delta_time < 5
|
||||
return true
|
||||
end
|
||||
|
||||
self.last_update = current_time
|
||||
|
||||
# Check if strip can accept updates
|
||||
|
||||
@ -27,29 +27,20 @@ class Token
|
||||
|
||||
static var statement_keywords = [
|
||||
"strip", "set", "color", "palette", "animation",
|
||||
"sequence", "function", "zone", "on", "run", "template", "param", "import", "berry"
|
||||
"sequence", "function", "on", "run", "template", "param", "import", "berry"
|
||||
]
|
||||
|
||||
static var keywords = [
|
||||
# Configuration keywords
|
||||
"strip", "set", "import", "berry",
|
||||
"strip", "set", "import", "berry", "extern",
|
||||
|
||||
# Definition keywords
|
||||
"color", "palette", "animation", "sequence", "function", "zone", "template", "param", "type",
|
||||
"color", "palette", "animation", "sequence", "function", "template", "param", "type",
|
||||
|
||||
# Control flow keywords
|
||||
"play", "for", "with", "repeat", "times", "forever", "if", "else", "elif",
|
||||
"choose", "random", "on", "run", "wait", "goto", "interrupt", "resume",
|
||||
"while", "from", "to", "return", "reset", "restart",
|
||||
|
||||
# Modifier keywords (only actual DSL syntax keywords)
|
||||
"at", "ease", "sync", "every", "stagger", "across", "pixels",
|
||||
|
||||
# Core built-in functions (minimal set for essential DSL operations)
|
||||
"rgb", "hsv",
|
||||
|
||||
# Spatial keywords
|
||||
"all", "even", "odd", "center", "edges", "left", "right", "top", "bottom",
|
||||
"while", "from", "to", "return", "reset", "restart", "every",
|
||||
|
||||
# Boolean and special values
|
||||
"true", "false", "nil", "transparent",
|
||||
@ -59,7 +50,7 @@ class Token
|
||||
"brightness_change", "timer", "time", "sound_peak", "network_message",
|
||||
|
||||
# Time and measurement keywords
|
||||
"ms", "s", "m", "h", "bpm"
|
||||
"ms", "s", "m", "h"
|
||||
]
|
||||
|
||||
static var color_names = [
|
||||
|
||||
@ -508,6 +508,8 @@ class SimpleDSLTranspiler
|
||||
self.process_event_handler()
|
||||
elif tok.value == "berry"
|
||||
self.process_berry_code_block()
|
||||
elif tok.value == "extern"
|
||||
self.process_external_function()
|
||||
else
|
||||
self.error(f"Unknown keyword '{tok.value}'.")
|
||||
self.skip_statement()
|
||||
@ -1755,6 +1757,13 @@ class SimpleDSLTranspiler
|
||||
return self.ExpressionResult.literal(entry.get_reference(), 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#)
|
||||
end
|
||||
|
||||
# Special handling for user functions used without parentheses
|
||||
if entry.is_user_function()
|
||||
# User function used without parentheses - call it with engine parameter
|
||||
var result = f"animation.get_user_function('{name}')(engine)"
|
||||
return self.ExpressionResult.function_call(result)
|
||||
end
|
||||
|
||||
# Regular identifier - check if it's a variable reference
|
||||
var ref = self.symbol_table.get_reference(name)
|
||||
var return_type = self._determine_symbol_return_type(entry) # compute the return type based on entry
|
||||
@ -2693,6 +2702,45 @@ class SimpleDSLTranspiler
|
||||
self.add("# End berry code block")
|
||||
end
|
||||
|
||||
# Process external function declaration: extern function function_name
|
||||
def process_external_function()
|
||||
self.next() # skip 'extern'
|
||||
|
||||
# Expect 'function' keyword
|
||||
var tok = self.current()
|
||||
if tok == nil || tok.type != 0 #-animation_dsl.Token.KEYWORD-# || tok.value != "function"
|
||||
self.error("Expected 'function' keyword after 'extern'. Use: extern function function_name")
|
||||
self.skip_statement()
|
||||
return
|
||||
end
|
||||
|
||||
self.next() # skip 'function'
|
||||
|
||||
# Expect an identifier for the function name
|
||||
tok = self.current()
|
||||
if tok == nil || tok.type != 1 #-animation_dsl.Token.IDENTIFIER-#
|
||||
self.error("Expected function name after 'extern function'. Use: extern function function_name")
|
||||
self.skip_statement()
|
||||
return
|
||||
end
|
||||
|
||||
var func_name = tok.value
|
||||
self.next() # consume identifier token
|
||||
|
||||
var inline_comment = self.collect_inline_comment()
|
||||
|
||||
# Validate function name
|
||||
self.validate_user_name(func_name, "extern function")
|
||||
|
||||
# Register the function as a user function in the symbol table
|
||||
# This allows it to be used in computed parameters and function calls
|
||||
self.symbol_table.register_user_function(func_name)
|
||||
|
||||
# Generate runtime registration call so the function is available at execution time
|
||||
self.add(f"# External function declaration: {func_name}{inline_comment}")
|
||||
self.add(f"animation.register_user_function(\"{func_name}\", {func_name})")
|
||||
end
|
||||
|
||||
# Generate default strip initialization using Tasmota configuration
|
||||
def generate_default_strip_initialization()
|
||||
if self.strip_initialized
|
||||
|
||||
@ -34,11 +34,11 @@ class OscillatorValueProvider : animation.value_provider
|
||||
# Parameter definitions for the oscillator
|
||||
static var PARAMS = animation.enc_params({
|
||||
"min_value": {"default": 0},
|
||||
"max_value": {"default": 100},
|
||||
"max_value": {"default": 255},
|
||||
"duration": {"min": 1, "default": 1000},
|
||||
"form": {"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9], "default": 1},
|
||||
"phase": {"min": 0, "max": 100, "default": 0},
|
||||
"duty_cycle": {"min": 0, "max": 100, "default": 50}
|
||||
"phase": {"min": 0, "max": 255, "default": 0},
|
||||
"duty_cycle": {"min": 0, "max": 255, "default": 127}
|
||||
})
|
||||
|
||||
# Initialize a new OscillatorValueProvider
|
||||
@ -92,7 +92,7 @@ class OscillatorValueProvider : animation.value_provider
|
||||
past = 0
|
||||
end
|
||||
|
||||
var duration_ms_mid = tasmota.scale_uint(duty_cycle, 0, 100, 0, duration)
|
||||
var duration_ms_mid = tasmota.scale_uint(duty_cycle, 0, 255, 0, duration)
|
||||
|
||||
# Handle cycle wrapping
|
||||
if past >= duration
|
||||
@ -105,7 +105,7 @@ class OscillatorValueProvider : animation.value_provider
|
||||
|
||||
# Apply phase shift
|
||||
if phase > 0
|
||||
past_with_phase += tasmota.scale_uint(phase, 0, 100, 0, duration)
|
||||
past_with_phase += tasmota.scale_uint(phase, 0, 255, 0, duration)
|
||||
if past_with_phase >= duration
|
||||
past_with_phase -= duration
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -209,6 +209,9 @@ class MockDynamicStrip
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
def push_pixels_buffer_argb()
|
||||
end
|
||||
|
||||
def show()
|
||||
self.show_calls += 1
|
||||
@ -287,9 +290,12 @@ assert_test(new_show_calls >= old_show_calls, "Strip should be updated after len
|
||||
# Test 10d: Multiple length changes
|
||||
print("\n--- Test 10d: Multiple length changes ---")
|
||||
var lengths_to_test = [10, 50, 5, 30]
|
||||
var base_tick_time = int(tasmota.millis()) + 5000 # Start well after previous tests
|
||||
var tick_offset = 0
|
||||
for new_length : lengths_to_test
|
||||
dynamic_strip.set_length(new_length)
|
||||
dynamic_engine.on_tick(tasmota.millis())
|
||||
dynamic_engine.on_tick(base_tick_time + tick_offset)
|
||||
tick_offset += 100 # Space ticks 100ms apart to avoid throttling
|
||||
assert_equals(dynamic_engine.strip_length, new_length, f"Engine should adapt to length {new_length}")
|
||||
assert_equals(dynamic_engine.frame_buffer.width, new_length, f"Frame buffer should adapt to length {new_length}")
|
||||
assert_equals(dynamic_engine.temp_buffer.width, new_length, f"Temp buffer should adapt to length {new_length}")
|
||||
@ -315,7 +321,8 @@ assert_equals(dynamic_engine.size(), 2, "Should have 2 animations")
|
||||
# Change length and verify all animations continue working
|
||||
dynamic_strip.set_length(40)
|
||||
old_show_calls = dynamic_strip.show_calls
|
||||
dynamic_engine.on_tick(tasmota.millis())
|
||||
# Use a time that's guaranteed to be past the throttle window
|
||||
dynamic_engine.on_tick(int(tasmota.millis()) + 10000)
|
||||
|
||||
assert_equals(dynamic_engine.strip_length, 40, "Engine should handle length change with multiple animations")
|
||||
new_show_calls = dynamic_strip.show_calls
|
||||
@ -325,20 +332,21 @@ assert_equals(dynamic_engine.size(), 2, "Should still have 2 animations after le
|
||||
# Test 10f: Invalid length handling
|
||||
print("\n--- Test 10f: Invalid length handling ---")
|
||||
var current_width = dynamic_engine.strip_length
|
||||
var invalid_test_time = int(tasmota.millis()) + 15000
|
||||
|
||||
# Test zero length (should be ignored)
|
||||
dynamic_strip.set_length(0)
|
||||
dynamic_engine.on_tick(tasmota.millis())
|
||||
dynamic_engine.on_tick(invalid_test_time)
|
||||
assert_equals(dynamic_engine.strip_length, current_width, "Should ignore zero length")
|
||||
|
||||
# Test negative length (should be ignored)
|
||||
dynamic_strip.set_length(-5)
|
||||
dynamic_engine.on_tick(tasmota.millis())
|
||||
dynamic_engine.on_tick(invalid_test_time + 100)
|
||||
assert_equals(dynamic_engine.strip_length, current_width, "Should ignore negative length")
|
||||
|
||||
# Restore valid length
|
||||
dynamic_strip.set_length(20)
|
||||
dynamic_engine.on_tick(tasmota.millis())
|
||||
dynamic_engine.on_tick(invalid_test_time + 200)
|
||||
assert_equals(dynamic_engine.strip_length, 20, "Should accept valid length after invalid ones")
|
||||
|
||||
# Test 10g: Performance impact of length checking
|
||||
@ -366,6 +374,159 @@ assert_test(changing_time < 200, f"20 ticks with length changes should be reason
|
||||
|
||||
dynamic_engine.stop()
|
||||
|
||||
# Test 11: Tick Interval Configuration
|
||||
print("\n--- Test 11: Tick Interval Configuration ---")
|
||||
|
||||
# Test 11a: Static default value
|
||||
print("\n--- Test 11a: Static default value ---")
|
||||
assert_equals(animation.create_engine.TICK_MS, 50, "Static TICK_MS should default to 50ms")
|
||||
|
||||
# Test 11b: Instance initialization from static default
|
||||
print("\n--- Test 11b: Instance initialization from static default ---")
|
||||
var tick_strip = global.Leds(10)
|
||||
var tick_engine = animation.create_engine(tick_strip)
|
||||
assert_equals(tick_engine.tick_ms, 50, "Instance tick_ms should initialize to static default (50ms)")
|
||||
|
||||
# Test 11c: Runtime modification
|
||||
print("\n--- Test 11c: Runtime modification ---")
|
||||
tick_engine.tick_ms = 100
|
||||
assert_equals(tick_engine.tick_ms, 100, "Should be able to change tick_ms at runtime to 100ms")
|
||||
|
||||
tick_engine.tick_ms = 25
|
||||
assert_equals(tick_engine.tick_ms, 25, "Should be able to change tick_ms at runtime to 25ms")
|
||||
|
||||
tick_engine.tick_ms = 5
|
||||
assert_equals(tick_engine.tick_ms, 5, "Should be able to change tick_ms at runtime to 5ms")
|
||||
|
||||
# Test 11d: Throttling behavior with different tick_ms values
|
||||
print("\n--- Test 11d: Throttling behavior with different tick_ms values ---")
|
||||
|
||||
# Create a mock strip to track show() calls
|
||||
class ThrottleTestStrip
|
||||
var _length
|
||||
var show_calls
|
||||
var last_show_time
|
||||
|
||||
def init(length)
|
||||
self._length = length
|
||||
self.show_calls = 0
|
||||
self.last_show_time = 0
|
||||
end
|
||||
|
||||
def length()
|
||||
return self._length
|
||||
end
|
||||
|
||||
def set_pixel_color(index, color)
|
||||
end
|
||||
|
||||
def clear()
|
||||
end
|
||||
|
||||
def push_pixels_buffer_argb(buffer)
|
||||
end
|
||||
|
||||
def show()
|
||||
self.show_calls += 1
|
||||
self.last_show_time = tasmota.millis()
|
||||
end
|
||||
|
||||
def can_show()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
var throttle_strip = ThrottleTestStrip(10)
|
||||
var throttle_engine = animation.create_engine(throttle_strip)
|
||||
|
||||
# Add a simple animation
|
||||
var throttle_anim = animation.solid(throttle_engine)
|
||||
throttle_anim.color = 0xFFFF0000
|
||||
throttle_engine.add(throttle_anim)
|
||||
throttle_engine.run()
|
||||
|
||||
# Test with 50ms throttle (default)
|
||||
print("\n--- Testing with 50ms throttle ---")
|
||||
throttle_engine.tick_ms = 50
|
||||
throttle_strip.show_calls = 0
|
||||
var base_time = int(tasmota.millis()) + 10000 # Start well after any previous ticks
|
||||
|
||||
# Simulate rapid ticks within throttle window (should be throttled)
|
||||
throttle_engine.on_tick(base_time)
|
||||
var initial_calls = throttle_strip.show_calls
|
||||
throttle_engine.on_tick(base_time + 10) # +10ms - should be throttled
|
||||
throttle_engine.on_tick(base_time + 20) # +20ms - should be throttled
|
||||
throttle_engine.on_tick(base_time + 40) # +40ms - should be throttled
|
||||
var throttled_calls = throttle_strip.show_calls
|
||||
assert_test(throttled_calls <= initial_calls + 1, f"Ticks within 50ms window should be throttled (got {throttled_calls - initial_calls} additional calls)")
|
||||
|
||||
# Tick after throttle window (should render)
|
||||
throttle_engine.on_tick(base_time + 60) # +60ms - should render
|
||||
var after_throttle_calls = throttle_strip.show_calls
|
||||
# Debug: print the call counts
|
||||
# print(f"DEBUG: initial={initial_calls}, throttled={throttled_calls}, after={after_throttle_calls}")
|
||||
assert_test(after_throttle_calls > throttled_calls, f"Tick after throttle window should render (initial={initial_calls}, throttled={throttled_calls}, after={after_throttle_calls})")
|
||||
|
||||
# Test with 100ms throttle
|
||||
print("\n--- Testing with 100ms throttle ---")
|
||||
throttle_engine.tick_ms = 100
|
||||
throttle_strip.show_calls = 0
|
||||
base_time = int(tasmota.millis()) + 20000 # Start well after previous test
|
||||
|
||||
throttle_engine.on_tick(base_time)
|
||||
var initial_calls_100 = throttle_strip.show_calls
|
||||
throttle_engine.on_tick(base_time + 50) # +50ms - should be throttled
|
||||
throttle_engine.on_tick(base_time + 80) # +80ms - should be throttled
|
||||
throttled_calls = throttle_strip.show_calls
|
||||
assert_test(throttled_calls <= initial_calls_100 + 1, f"Ticks within 100ms window should be throttled (got {throttled_calls - initial_calls_100} additional calls)")
|
||||
|
||||
throttle_engine.on_tick(base_time + 110) # +110ms - should render
|
||||
after_throttle_calls = throttle_strip.show_calls
|
||||
assert_test(after_throttle_calls > throttled_calls, "Tick after 100ms throttle window should render")
|
||||
|
||||
# Test with 10ms throttle (faster updates)
|
||||
print("\n--- Testing with 10ms throttle ---")
|
||||
throttle_engine.tick_ms = 10
|
||||
throttle_strip.show_calls = 0
|
||||
base_time = int(tasmota.millis()) + 30000 # Start well after previous test
|
||||
|
||||
throttle_engine.on_tick(base_time)
|
||||
var initial_calls_10 = throttle_strip.show_calls
|
||||
throttle_engine.on_tick(base_time + 5) # +5ms - should be throttled
|
||||
var fast_throttled = throttle_strip.show_calls
|
||||
assert_test(fast_throttled <= initial_calls_10 + 1, f"Ticks within 10ms window should be throttled (got {fast_throttled - initial_calls_10} additional calls)")
|
||||
|
||||
throttle_engine.on_tick(base_time + 15) # +15ms - should render
|
||||
var fast_after = throttle_strip.show_calls
|
||||
assert_test(fast_after > fast_throttled, "Tick after 10ms throttle window should render")
|
||||
|
||||
# Test 11e: Independent engine instances
|
||||
print("\n--- Test 11e: Independent engine instances ---")
|
||||
var strip_a = global.Leds(10)
|
||||
var strip_b = global.Leds(10)
|
||||
var engine_a = animation.create_engine(strip_a)
|
||||
var engine_b = animation.create_engine(strip_b)
|
||||
|
||||
# Set different tick_ms values
|
||||
engine_a.tick_ms = 25
|
||||
engine_b.tick_ms = 75
|
||||
|
||||
assert_equals(engine_a.tick_ms, 25, "Engine A should have tick_ms of 25ms")
|
||||
assert_equals(engine_b.tick_ms, 75, "Engine B should have tick_ms of 75ms")
|
||||
assert_test(engine_a.tick_ms != engine_b.tick_ms, "Different engine instances should have independent tick_ms values")
|
||||
|
||||
# Test 11f: Tick interval doesn't affect static default
|
||||
print("\n--- Test 11f: Tick interval doesn't affect static default ---")
|
||||
var test_engine = animation.create_engine(global.Leds(10))
|
||||
test_engine.tick_ms = 200
|
||||
assert_equals(animation.create_engine.TICK_MS, 50, "Changing instance tick_ms should not affect static TICK_MS")
|
||||
|
||||
# New engine should still use static default
|
||||
var new_engine = animation.create_engine(global.Leds(10))
|
||||
assert_equals(new_engine.tick_ms, 50, "New engine should initialize with static default, not modified instance value")
|
||||
|
||||
throttle_engine.stop()
|
||||
|
||||
# Cleanup
|
||||
engine.stop()
|
||||
|
||||
|
||||
@ -274,7 +274,6 @@ def test_closure_math_methods()
|
||||
|
||||
# Test 1: min/max functions
|
||||
provider.closure = def(self, name, time_ms)
|
||||
print(f">> {name=} {animation._math=}")
|
||||
if name == "min_test"
|
||||
return animation._math.min(5, 3, 8, 1, 9) # Should return 1
|
||||
elif name == "max_test"
|
||||
|
||||
@ -77,6 +77,7 @@ print("\n--- Test 4: Timestamps Set During Ticks ---")
|
||||
# Create a fresh engine for timestamp testing with an animation
|
||||
var ts_strip = global.Leds(20)
|
||||
var ts_engine = animation.create_engine(ts_strip)
|
||||
ts_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
|
||||
# Add an animation so rendering happens
|
||||
var ts_anim = animation.solid(ts_engine)
|
||||
@ -85,7 +86,7 @@ ts_engine.add(ts_anim)
|
||||
ts_engine.run()
|
||||
|
||||
# Run a single tick
|
||||
var current_time = tasmota.millis()
|
||||
var current_time = int(tasmota.millis())
|
||||
ts_engine.on_tick(current_time)
|
||||
|
||||
# Check that timestamps were set
|
||||
@ -121,10 +122,11 @@ print("\n--- Test 5: Phase Metrics Accumulation ---")
|
||||
# Create engine and run multiple ticks
|
||||
var phase_strip = global.Leds(15)
|
||||
var phase_engine = animation.create_engine(phase_strip)
|
||||
phase_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
phase_engine.run()
|
||||
|
||||
# Run 10 ticks
|
||||
var phase_time = 0
|
||||
var phase_time = int(tasmota.millis())
|
||||
for i : 0..9
|
||||
phase_engine.on_tick(phase_time)
|
||||
phase_time += 5
|
||||
@ -148,9 +150,10 @@ print("\n--- Test 6: Timestamp-Based Duration Calculation ---")
|
||||
# Create engine and run a tick
|
||||
var dur_strip = global.Leds(10)
|
||||
var dur_engine = animation.create_engine(dur_strip)
|
||||
dur_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
dur_engine.run()
|
||||
|
||||
var dur_time = tasmota.millis()
|
||||
var dur_time = int(tasmota.millis())
|
||||
dur_engine.on_tick(dur_time)
|
||||
|
||||
# Verify durations can be computed from timestamps
|
||||
@ -177,6 +180,7 @@ print("\n--- Test 7: CPU Metrics During Ticks ---")
|
||||
# Create a fresh engine for tick testing
|
||||
var tick_strip = global.Leds(20)
|
||||
var tick_engine = animation.create_engine(tick_strip)
|
||||
tick_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
|
||||
# Add a simple animation
|
||||
var test_anim = animation.solid(tick_engine)
|
||||
@ -185,7 +189,7 @@ tick_engine.add(test_anim)
|
||||
tick_engine.run()
|
||||
|
||||
# Simulate several ticks
|
||||
var current_time = tasmota.millis()
|
||||
var current_time = int(tasmota.millis())
|
||||
for i : 0..9
|
||||
tick_engine.on_tick(current_time + i * 10)
|
||||
end
|
||||
@ -200,12 +204,13 @@ print("\n--- Test 8: Metrics Reset After Stats Period ---")
|
||||
# Create engine and simulate ticks over stats period
|
||||
var reset_strip = global.Leds(15)
|
||||
var reset_engine = animation.create_engine(reset_strip)
|
||||
reset_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
reset_engine.run()
|
||||
|
||||
# Simulate ticks for just under 5 seconds
|
||||
var start_time = 0
|
||||
var start_time = int(tasmota.millis())
|
||||
var current_time = start_time
|
||||
while current_time < 4900
|
||||
while current_time < start_time + 4900
|
||||
reset_engine.on_tick(current_time)
|
||||
current_time += 5
|
||||
end
|
||||
@ -217,7 +222,7 @@ assert_greater_than(tick_count_before, 0, "Should have ticks before stats period
|
||||
var last_stats_before = reset_engine.last_stats_time
|
||||
|
||||
# Simulate more ticks to cross the 5 second threshold
|
||||
while current_time < 5100
|
||||
while current_time < start_time + 5100
|
||||
reset_engine.on_tick(current_time)
|
||||
current_time += 5
|
||||
end
|
||||
@ -235,10 +240,11 @@ print("\n--- Test 9: Metrics Consistency Across Ticks ---")
|
||||
|
||||
var consistency_strip = global.Leds(25)
|
||||
var consistency_engine = animation.create_engine(consistency_strip)
|
||||
consistency_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
consistency_engine.run()
|
||||
|
||||
# Run multiple ticks and verify metrics consistency
|
||||
var cons_time = 0
|
||||
var cons_time = int(tasmota.millis())
|
||||
for i : 0..19
|
||||
consistency_engine.on_tick(cons_time)
|
||||
cons_time += 5
|
||||
@ -258,6 +264,7 @@ print("\n--- Test 10: Min/Max Tracking for All Metrics ---")
|
||||
|
||||
var minmax_strip = global.Leds(10)
|
||||
var minmax_engine = animation.create_engine(minmax_strip)
|
||||
minmax_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
|
||||
# Add an animation so rendering happens
|
||||
var mm_anim = animation.solid(minmax_engine)
|
||||
@ -266,7 +273,7 @@ minmax_engine.add(mm_anim)
|
||||
minmax_engine.run()
|
||||
|
||||
# Run several ticks
|
||||
var mm_time = 0
|
||||
var mm_time = int(tasmota.millis())
|
||||
for i : 0..9
|
||||
minmax_engine.on_tick(mm_time)
|
||||
mm_time += 5
|
||||
@ -287,10 +294,11 @@ print("\n--- Test 11: Streaming Statistics Accuracy ---")
|
||||
|
||||
var stats_strip = global.Leds(15)
|
||||
var stats_engine = animation.create_engine(stats_strip)
|
||||
stats_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
stats_engine.run()
|
||||
|
||||
# Run exactly 10 ticks
|
||||
var stats_time = 0
|
||||
var stats_time = int(tasmota.millis())
|
||||
for i : 0..9
|
||||
stats_engine.on_tick(stats_time)
|
||||
stats_time += 5
|
||||
@ -306,10 +314,11 @@ print("\n--- Test 12: Phase Metrics Cleared After Stats ---")
|
||||
|
||||
var clear_strip = global.Leds(20)
|
||||
var clear_engine = animation.create_engine(clear_strip)
|
||||
clear_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
clear_engine.run()
|
||||
|
||||
# Run some ticks to accumulate phase metrics
|
||||
var clear_time = 0
|
||||
var clear_time = int(tasmota.millis())
|
||||
for i : 0..9
|
||||
clear_engine.on_tick(clear_time)
|
||||
clear_time += 5
|
||||
@ -319,7 +328,8 @@ end
|
||||
assert_greater_than(clear_engine.phase1_time_sum, -1, "Phase metrics should accumulate")
|
||||
|
||||
# Simulate ticks to cross stats period
|
||||
while clear_time < 5100
|
||||
var clear_start = clear_time
|
||||
while clear_time < clear_start + 5100
|
||||
clear_engine.on_tick(clear_time)
|
||||
clear_time += 5
|
||||
end
|
||||
@ -336,15 +346,17 @@ print("\n--- Test 13: Multiple Engines Independence ---")
|
||||
|
||||
var strip1 = global.Leds(10)
|
||||
var engine1 = animation.create_engine(strip1)
|
||||
engine1.tick_ms = 5 # Set low tick interval for testing
|
||||
engine1.run()
|
||||
|
||||
var strip2 = global.Leds(20)
|
||||
var engine2 = animation.create_engine(strip2)
|
||||
engine2.tick_ms = 5 # Set low tick interval for testing
|
||||
engine2.run()
|
||||
|
||||
# Run ticks on both engines
|
||||
var e1_time = 0
|
||||
var e2_time = 0
|
||||
var e1_time = int(tasmota.millis())
|
||||
var e2_time = int(tasmota.millis())
|
||||
|
||||
for i : 0..4
|
||||
engine1.on_tick(e1_time)
|
||||
@ -385,10 +397,12 @@ print("\n--- Test 15: Performance of Metrics Collection ---")
|
||||
|
||||
var perf_strip = global.Leds(30)
|
||||
var perf_engine = animation.create_engine(perf_strip)
|
||||
perf_engine.tick_ms = 5 # Set low tick interval for testing
|
||||
perf_engine.run()
|
||||
|
||||
# Measure overhead of metrics collection with timestamps
|
||||
var perf_start = tasmota.millis()
|
||||
var perf_start = int(tasmota.millis())
|
||||
var perf_time = perf_start
|
||||
for i : 0..99
|
||||
perf_engine.on_tick(perf_start + i * 5)
|
||||
end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# DSL Berry Code Blocks Test Suite
|
||||
# Tests for berry code block functionality in SimpleDSLTranspiler
|
||||
# DSL Berry Code Blocks and External Functions Test Suite
|
||||
# Tests for berry code block functionality and external function declarations in SimpleDSLTranspiler
|
||||
#
|
||||
# Command to run test is:
|
||||
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota" lib/libesp32/berry_animation/src/tests/dsl_berry_code_blocks_test.be
|
||||
@ -275,9 +275,284 @@ def test_multiline_complex_syntax()
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function declaration - basic syntax
|
||||
def test_external_function_basic()
|
||||
print("Testing basic external function declaration...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def test_func()\n' +
|
||||
' return 100\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function test_func\n' +
|
||||
'animation test = solid(color=red)\n' +
|
||||
'test.opacity = test_func\n' +
|
||||
'run test'
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "def test_func()") >= 0, "Should include function definition")
|
||||
assert(string.find(berry_code, "# External function declaration: test_func") >= 0, "Should have external declaration comment")
|
||||
assert(string.find(berry_code, "animation.get_user_function('test_func')(engine)") >= 0, "Should generate correct function call")
|
||||
|
||||
print("✓ Basic external function declaration test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function with parentheses
|
||||
def test_external_function_with_parentheses()
|
||||
print("Testing external function with parentheses...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def paren_func()\n' +
|
||||
' return 150\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function paren_func\n' +
|
||||
'animation test = solid(color=blue)\n' +
|
||||
'test.opacity = paren_func()\n' +
|
||||
'run test'
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "animation.get_user_function('paren_func')(engine)") >= 0, "Should generate correct function call with parentheses")
|
||||
|
||||
print("✓ External function with parentheses test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test multiple external functions
|
||||
def test_multiple_external_functions()
|
||||
print("Testing multiple external functions...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def func1()\n' +
|
||||
' return 100\n' +
|
||||
'end\n' +
|
||||
'def func2()\n' +
|
||||
' return 200\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function func1\n' +
|
||||
'extern function func2\n' +
|
||||
'animation a1 = solid(color=red)\n' +
|
||||
'a1.opacity = func1\n' +
|
||||
'animation a2 = solid(color=blue)\n' +
|
||||
'a2.opacity = func2\n' +
|
||||
'run a1'
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "animation.get_user_function('func1')(engine)") >= 0, "Should generate call for func1")
|
||||
assert(string.find(berry_code, "animation.get_user_function('func2')(engine)") >= 0, "Should generate call for func2")
|
||||
|
||||
print("✓ Multiple external functions test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function in arithmetic expressions
|
||||
def test_external_function_in_arithmetic()
|
||||
print("Testing external function in arithmetic expressions...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def math_func()\n' +
|
||||
' return 50\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function math_func\n' +
|
||||
'animation test = solid(color=green)\n' +
|
||||
'test.opacity = max(100, math_func + 50)\n' +
|
||||
'run test'
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "animation.get_user_function('math_func')(engine)") >= 0, "Should generate function call in arithmetic")
|
||||
assert(string.find(berry_code, "animation._math.max(") >= 0, "Should include math function")
|
||||
|
||||
print("✓ External function in arithmetic expressions test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function with complex berry code
|
||||
def test_external_function_complex()
|
||||
print("Testing external function with complex berry code...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'import math\n' +
|
||||
'def rand_meter(time_ms, self)\n' +
|
||||
' var r = math.rand() % 101\n' +
|
||||
' return r\n' +
|
||||
'end\n' +
|
||||
'def breathing_effect(base_value, amplitude)\n' +
|
||||
' var time_factor = (tasmota.millis() / 1000) % 4\n' +
|
||||
' var breath = math.sin(time_factor * math.pi / 2)\n' +
|
||||
' return int(base_value + breath * amplitude)\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function rand_meter\n' +
|
||||
'extern function breathing_effect\n' +
|
||||
'palette rainbow = [0xFF0000, 0x00FF00, 0x0000FF]\n' +
|
||||
'animation meter = palette_meter_animation(value_func = rand_meter)\n' +
|
||||
'animation breath = solid(color=blue)\n' +
|
||||
'breath.opacity = breathing_effect\n' +
|
||||
'run meter'
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "def rand_meter(time_ms, self)") >= 0, "Should include complex function definition")
|
||||
assert(string.find(berry_code, "def breathing_effect(base_value, amplitude)") >= 0, "Should include second function")
|
||||
assert(string.find(berry_code, "animation.get_user_function('rand_meter')(engine)") >= 0, "Should call rand_meter")
|
||||
assert(string.find(berry_code, "animation.get_user_function('breathing_effect')(engine)") >= 0, "Should call breathing_effect")
|
||||
|
||||
print("✓ External function with complex berry code test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test error handling - missing 'function' keyword
|
||||
def test_external_error_missing_function_keyword()
|
||||
print("Testing error handling for missing 'function' keyword...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def test_func()\n' +
|
||||
' return 100\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern test_func\n' +
|
||||
'animation test = solid(color=red)\n' +
|
||||
'run test'
|
||||
|
||||
try
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
assert(false, "Should raise compilation error for missing 'function' keyword")
|
||||
except "dsl_compilation_error" as e, msg
|
||||
assert(string.find(msg, "Expected 'function' keyword after 'extern'") >= 0, "Should have helpful error message")
|
||||
end
|
||||
|
||||
print("✓ Error handling (missing 'function' keyword) test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test error handling - missing function name
|
||||
def test_external_error_missing_function_name()
|
||||
print("Testing error handling for missing function name...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def test_func()\n' +
|
||||
' return 100\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function\n' +
|
||||
'animation test = solid(color=red)\n' +
|
||||
'run test'
|
||||
|
||||
try
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
assert(false, "Should raise compilation error for missing function name")
|
||||
except "dsl_compilation_error" as e, msg
|
||||
assert(string.find(msg, "Expected function name after 'extern function'") >= 0, "Should have helpful error message")
|
||||
end
|
||||
|
||||
print("✓ Error handling (missing function name) test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function with reserved name validation
|
||||
def test_external_function_reserved_name_validation()
|
||||
print("Testing external function with reserved name validation...")
|
||||
|
||||
# Test with a name that conflicts with an existing definition
|
||||
var dsl_source = 'color my_color = 0xFF0000\n' +
|
||||
'berry """\n' +
|
||||
'def my_color()\n' +
|
||||
' return 100\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function my_color\n' +
|
||||
'animation test = solid(color=blue)\n' +
|
||||
'run test'
|
||||
|
||||
try
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
assert(false, "Should raise compilation error for already defined name")
|
||||
except "dsl_compilation_error" as e, msg
|
||||
# Check for redefinition error
|
||||
var has_error = string.find(msg, "already defined") >= 0 ||
|
||||
string.find(msg, "redefine") >= 0 ||
|
||||
string.find(msg, "my_color") >= 0
|
||||
if !has_error
|
||||
print(f"Unexpected error message: {msg}")
|
||||
assert(false, f"Should reject already defined names, got: {msg}")
|
||||
end
|
||||
end
|
||||
|
||||
print("✓ External function reserved name validation test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function in sequences
|
||||
def test_external_function_in_sequences()
|
||||
print("Testing external function in sequences...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def seq_func()\n' +
|
||||
' return 180\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function seq_func\n' +
|
||||
'animation test = solid(color=purple)\n' +
|
||||
'sequence demo {\n' +
|
||||
' test.opacity = seq_func\n' +
|
||||
' play test for 2s\n' +
|
||||
'}\n' +
|
||||
'run demo'
|
||||
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
|
||||
assert(berry_code != nil, "Should generate Berry code")
|
||||
assert(string.find(berry_code, "animation.get_user_function('seq_func')(engine)") >= 0, "Should call external function in sequence")
|
||||
|
||||
print("✓ External function in sequences test passed")
|
||||
return true
|
||||
end
|
||||
|
||||
# Test external function compilation and execution
|
||||
def test_external_function_execution()
|
||||
print("Testing external function compilation and execution...")
|
||||
|
||||
var dsl_source = 'berry """\n' +
|
||||
'def exec_test_func()\n' +
|
||||
' print("External function executed successfully")\n' +
|
||||
' return 128\n' +
|
||||
'end\n' +
|
||||
'"""\n' +
|
||||
'extern function exec_test_func\n' +
|
||||
'animation test = solid(color=cyan)\n' +
|
||||
'test.opacity = exec_test_func\n' +
|
||||
'run test'
|
||||
|
||||
# Test compilation
|
||||
var berry_code = animation_dsl.compile(dsl_source)
|
||||
assert(berry_code != nil, "Should compile successfully")
|
||||
|
||||
# Test that generated code compiles
|
||||
try
|
||||
compile(berry_code)
|
||||
print("✓ External function execution test passed")
|
||||
return true
|
||||
except .. as e, m
|
||||
print("✗ Generated code compilation failed:", e, m)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Run all tests
|
||||
def run_all_berry_block_tests()
|
||||
print("=== DSL Berry Code Blocks Test Suite ===")
|
||||
print("=== DSL Berry Code Blocks and External Functions Test Suite ===")
|
||||
print("")
|
||||
|
||||
var tests = [
|
||||
@ -290,7 +565,17 @@ def run_all_berry_block_tests()
|
||||
test_error_missing_string,
|
||||
test_error_invalid_token,
|
||||
test_berry_block_execution,
|
||||
test_multiline_complex_syntax
|
||||
test_multiline_complex_syntax,
|
||||
test_external_function_basic,
|
||||
test_external_function_with_parentheses,
|
||||
test_multiple_external_functions,
|
||||
test_external_function_in_arithmetic,
|
||||
test_external_function_complex,
|
||||
test_external_error_missing_function_keyword,
|
||||
test_external_error_missing_function_name,
|
||||
test_external_function_reserved_name_validation,
|
||||
test_external_function_in_sequences,
|
||||
test_external_function_execution
|
||||
]
|
||||
|
||||
var passed = 0
|
||||
@ -307,14 +592,14 @@ def run_all_berry_block_tests()
|
||||
print("")
|
||||
end
|
||||
|
||||
print("=== Berry Code Blocks Test Results ===")
|
||||
print("=== Berry Code Blocks and External Functions Test Results ===")
|
||||
print(f"Passed: {passed}/{total}")
|
||||
|
||||
if passed == total
|
||||
print("All berry code block tests passed! ✓")
|
||||
print("All berry code block and external function tests passed! ✓")
|
||||
return true
|
||||
else
|
||||
print("Some berry code block tests failed! ✗")
|
||||
print("Some berry code block or external function tests failed! ✗")
|
||||
raise "test_failed"
|
||||
end
|
||||
end
|
||||
|
||||
@ -337,7 +337,6 @@ def test_complex_dsl()
|
||||
"# Color Definitions\n" +
|
||||
"color red = 0xFF0000\n" +
|
||||
"color orange = rgb(255, 128, 0)\n" +
|
||||
"color yellow = hsv(60, 100, 100)\n" +
|
||||
"\n" +
|
||||
"# Animation Definitions\n" +
|
||||
"animation fire_gradient = gradient(color=red)\n" +
|
||||
|
||||
@ -75,6 +75,9 @@ def test_on_tick_performance()
|
||||
var strip = global.Leds(10)
|
||||
var engine = animation.create_engine(strip)
|
||||
|
||||
# Set tick_ms to 5 for testing (default is 50ms)
|
||||
engine.tick_ms = 5
|
||||
|
||||
# Add a test animation
|
||||
var anim = TestAnimation(engine)
|
||||
anim.priority = 1
|
||||
@ -89,7 +92,7 @@ def test_on_tick_performance()
|
||||
tasmota.set_millis(initial_time)
|
||||
engine.last_update = initial_time
|
||||
|
||||
# Call on_tick with less than 5ms elapsed
|
||||
# Call on_tick with less than 5ms elapsed (should be throttled)
|
||||
tasmota.set_millis(initial_time + 3)
|
||||
var result = engine.on_tick()
|
||||
|
||||
@ -97,7 +100,7 @@ def test_on_tick_performance()
|
||||
assert(result == true)
|
||||
assert(anim.render_called == false)
|
||||
|
||||
# Call on_tick with more than 5ms elapsed
|
||||
# Call on_tick with more than 5ms elapsed (should render)
|
||||
tasmota.set_millis(initial_time + 10)
|
||||
result = engine.on_tick()
|
||||
|
||||
|
||||
@ -156,7 +156,7 @@ def test_ease_with_phase()
|
||||
provider.min_value = 0
|
||||
provider.max_value = 100
|
||||
provider.duration = 1000
|
||||
provider.phase = 25 # 25% phase shift
|
||||
provider.phase = 64 # 25% phase shift (64 out of 255 is ~25%)
|
||||
provider.start(0) # Start at time 0
|
||||
|
||||
# With 25% phase shift, the curve should be shifted forward
|
||||
|
||||
@ -40,16 +40,16 @@ def test_oscillator_basic()
|
||||
assert(osc.duration == 1000, "Duration should be 1000ms")
|
||||
assert(osc.form == animation.SAWTOOTH, "Form should be SAWTOOTH")
|
||||
assert(osc.phase == 0, "Phase should default to 0")
|
||||
assert(osc.duty_cycle == 50, "Duty cycle should default to 50")
|
||||
assert(osc.duty_cycle == 127, "Duty cycle should default to 127")
|
||||
|
||||
# Test parameter modification
|
||||
osc.phase = 25
|
||||
osc.duty_cycle = 75
|
||||
osc.phase = 64
|
||||
osc.duty_cycle = 191
|
||||
osc.min_value = 10
|
||||
osc.max_value = 90
|
||||
|
||||
assert(osc.phase == 25, "Phase should be set to 25")
|
||||
assert(osc.duty_cycle == 75, "Duty cycle should be set to 75")
|
||||
assert(osc.phase == 64, "Phase should be set to 64")
|
||||
assert(osc.duty_cycle == 191, "Duty cycle should be set to 191")
|
||||
assert(osc.min_value == 10, "Starting value should be set to 10")
|
||||
assert(osc.max_value == 90, "End value should be set to 90")
|
||||
|
||||
@ -147,8 +147,8 @@ def test_square_waveform()
|
||||
assert(value_51 == 100, f"Value at 51% should be 100, got {value_51}")
|
||||
assert(value_75 == 100, f"Value at 75% should be 100, got {value_75}")
|
||||
|
||||
# Test custom duty cycle (25%)
|
||||
osc.duty_cycle = 25
|
||||
# Test custom duty cycle (25% = 64 out of 255)
|
||||
osc.duty_cycle = 64
|
||||
var value_20 = osc.produce_value("test", start_time + 200) # t=200ms (20% - first quarter)
|
||||
var value_30 = osc.produce_value("test", start_time + 300) # t=300ms (30% - second quarter)
|
||||
|
||||
@ -258,8 +258,8 @@ def test_phase_shift()
|
||||
osc.phase = 0
|
||||
var value_no_phase = osc.produce_value("test", start_time)
|
||||
|
||||
# Test with 25% phase shift (should be like starting at 25% of cycle)
|
||||
osc.phase = 25
|
||||
# Test with 25% phase shift (64 out of 255 is ~25%)
|
||||
osc.phase = 64
|
||||
var value_with_phase = osc.produce_value("test", start_time)
|
||||
|
||||
# Values should be different due to phase shift
|
||||
@ -330,16 +330,16 @@ def test_static_constructors()
|
||||
square1.min_value = 0
|
||||
square1.max_value = 1
|
||||
square1.duration = 500
|
||||
square1.duty_cycle = 30
|
||||
square1.duty_cycle = 76
|
||||
assert(square1.form == animation.SQUARE, "square() should use SQUARE")
|
||||
assert(square1.duty_cycle == 30, "square() should set duty cycle to 30")
|
||||
assert(square1.duty_cycle == 76, "square() should set duty cycle to 76")
|
||||
|
||||
# Test square() with default duty cycle
|
||||
var square2 = animation.square(mock_engine)
|
||||
square2.min_value = 0
|
||||
square2.max_value = 1
|
||||
square2.duration = 500
|
||||
assert(square2.duty_cycle == 50, "square() should default duty cycle to 50")
|
||||
assert(square2.duty_cycle == 127, "square() should default duty cycle to 127")
|
||||
|
||||
print("✓ Static constructor functions test passed")
|
||||
end
|
||||
@ -400,7 +400,7 @@ def test_edge_cases()
|
||||
# Test with default parameters
|
||||
var osc1 = animation.oscillator_value(mock_engine)
|
||||
assert(osc1.min_value == 0, "Default min_value should be 0")
|
||||
assert(osc1.max_value == 100, "Default max_value should be 100")
|
||||
assert(osc1.max_value == 255, "Default max_value should be 255")
|
||||
assert(osc1.duration == 1000, "Default duration should be 1000")
|
||||
assert(osc1.form == animation.SAWTOOTH, "Default form should be SAWTOOTH")
|
||||
|
||||
@ -420,14 +420,14 @@ def test_edge_cases()
|
||||
|
||||
# Test valid bounds
|
||||
osc3.phase = 0
|
||||
osc3.duty_cycle = 50
|
||||
osc3.duty_cycle = 127
|
||||
assert(osc3.phase == 0, "Phase 0 should be valid")
|
||||
assert(osc3.duty_cycle == 50, "Duty cycle 50 should be valid")
|
||||
assert(osc3.duty_cycle == 127, "Duty cycle 127 should be valid")
|
||||
|
||||
osc3.phase = 100
|
||||
osc3.duty_cycle = 100
|
||||
assert(osc3.phase == 100, "Phase 100 should be valid")
|
||||
assert(osc3.duty_cycle == 100, "Duty cycle 100 should be valid")
|
||||
osc3.phase = 255
|
||||
osc3.duty_cycle = 255
|
||||
assert(osc3.phase == 255, "Phase 255 should be valid")
|
||||
assert(osc3.duty_cycle == 255, "Duty cycle 255 should be valid")
|
||||
|
||||
print("✓ Edge cases test passed")
|
||||
end
|
||||
|
||||
@ -29,10 +29,6 @@ var frame = animation.frame_buffer(10, 1)
|
||||
# For simple testing, we'll use direct color values
|
||||
# More complex color providers can be tested separately
|
||||
|
||||
# Test 1: Basic PalettePatternAnimation with custom pattern function
|
||||
print("Test 1: Basic PalettePatternAnimation with custom pattern function")
|
||||
var pattern_anim = animation.palette_pattern_animation(mock_engine)
|
||||
|
||||
# Create a simple mock color source that has get_color_for_value method
|
||||
class MockColorSource
|
||||
def get_color_for_value(value, time_ms)
|
||||
@ -42,67 +38,7 @@ class MockColorSource
|
||||
end
|
||||
var mock_color_source = MockColorSource()
|
||||
|
||||
pattern_anim.color_source = mock_color_source
|
||||
pattern_anim.priority = 10
|
||||
pattern_anim.duration = 0
|
||||
pattern_anim.loop = false
|
||||
pattern_anim.opacity = 255
|
||||
pattern_anim.name = "pattern_test"
|
||||
|
||||
# Create a simple pattern function that alternates between 0 and 255
|
||||
def simple_pattern(pixel_index, time_ms, animation)
|
||||
return pixel_index % 2 == 0 ? 255 : 0
|
||||
end
|
||||
pattern_anim.pattern_func = simple_pattern
|
||||
|
||||
assert(pattern_anim != nil, "Failed to create pattern animation")
|
||||
|
||||
# Start the animation
|
||||
pattern_anim.start()
|
||||
pattern_anim.update() # force first tick
|
||||
assert(pattern_anim.is_running, "Animation should be running")
|
||||
|
||||
# Update and render
|
||||
pattern_anim.update(mock_engine.time_ms)
|
||||
frame.clear()
|
||||
var result = pattern_anim.render(frame, mock_engine.time_ms)
|
||||
assert(result, "Render should return true")
|
||||
|
||||
# Test 2: PaletteWaveAnimation
|
||||
print("Test 2: PaletteWaveAnimation")
|
||||
var wave_anim = animation.palette_wave_animation(mock_engine)
|
||||
wave_anim.color_source = mock_color_source
|
||||
wave_anim.wave_period = 2000 # 2 second wave period
|
||||
wave_anim.wave_length = 5 # Wave length of 5 pixels
|
||||
wave_anim.priority = 10
|
||||
wave_anim.duration = 0
|
||||
wave_anim.loop = false
|
||||
wave_anim.opacity = 255
|
||||
wave_anim.name = "wave_test"
|
||||
|
||||
assert(wave_anim != nil, "Failed to create wave animation")
|
||||
assert(wave_anim.wave_period == 2000, "Wave period should be 2000")
|
||||
assert(wave_anim.wave_length == 5, "Wave length should be 5")
|
||||
|
||||
# Start the animation
|
||||
wave_anim.start()
|
||||
wave_anim.update() # force first tick
|
||||
assert(wave_anim.is_running, "Animation should be running")
|
||||
|
||||
# Update and render
|
||||
wave_anim.update(mock_engine.time_ms)
|
||||
frame.clear()
|
||||
result = wave_anim.render(frame, mock_engine.time_ms)
|
||||
assert(result, "Render should return true")
|
||||
|
||||
# Test parameter changes
|
||||
wave_anim.wave_period = 1000
|
||||
assert(wave_anim.wave_period == 1000, "Wave period should be updated to 1000")
|
||||
|
||||
wave_anim.wave_length = 8
|
||||
assert(wave_anim.wave_length == 8, "Wave length should be updated to 8")
|
||||
|
||||
# Test 3: PaletteGradientAnimation
|
||||
# Test 1: PaletteGradientAnimation
|
||||
print("Test 3: PaletteGradientAnimation")
|
||||
var gradient_anim = animation.palette_gradient_animation(mock_engine)
|
||||
gradient_anim.color_source = mock_color_source
|
||||
@ -135,20 +71,20 @@ assert(gradient_anim.shift_period == 1500, "Shift period should be updated to 15
|
||||
gradient_anim.spatial_period = 5
|
||||
assert(gradient_anim.spatial_period == 5, "Spatial period should be updated to 5")
|
||||
|
||||
gradient_anim.phase_shift = 25
|
||||
assert(gradient_anim.phase_shift == 25, "Phase shift should be updated to 25")
|
||||
gradient_anim.phase_shift = 64
|
||||
assert(gradient_anim.phase_shift == 64, "Phase shift should be updated to 64")
|
||||
|
||||
# Test static gradient (shift_period = 0)
|
||||
gradient_anim.shift_period = 0
|
||||
assert(gradient_anim.shift_period == 0, "Shift period should be updated to 0 (static)")
|
||||
|
||||
# Test 4: PaletteMeterAnimation
|
||||
print("Test 4: PaletteMeterAnimation")
|
||||
# Test 2: PaletteMeterAnimation
|
||||
print("Test 2: PaletteMeterAnimation")
|
||||
var meter_anim = animation.palette_meter_animation(mock_engine)
|
||||
meter_anim.color_source = mock_color_source
|
||||
|
||||
# Create a value function that returns 50% (half the strip)
|
||||
def meter_value_func(time_ms, animation)
|
||||
def meter_value_func(engine, time_ms, animation)
|
||||
return 50 # 50% of the strip (this is still 0-100 for meter logic)
|
||||
end
|
||||
meter_anim.value_func = meter_value_func
|
||||
@ -173,7 +109,7 @@ result = meter_anim.render(frame, mock_engine.time_ms)
|
||||
assert(result, "Render should return true")
|
||||
|
||||
# Test changing value function
|
||||
def new_meter_value_func(time_ms, animation)
|
||||
def new_meter_value_func(engine, time_ms, animation)
|
||||
return 75 # 75% of the strip (this is still 0-100 for meter logic)
|
||||
end
|
||||
meter_anim.value_func = new_meter_value_func
|
||||
@ -183,12 +119,12 @@ frame.clear()
|
||||
result = meter_anim.render(frame, mock_engine.time_ms)
|
||||
assert(result, "Render should return true")
|
||||
|
||||
# Test 5: Changing color sources dynamically
|
||||
print("Test 5: Changing color sources dynamically")
|
||||
var dynamic_anim = animation.palette_wave_animation(mock_engine)
|
||||
# Test 3: Changing color sources dynamically
|
||||
print("Test 3: Changing color sources dynamically")
|
||||
var dynamic_anim = animation.palette_gradient_animation(mock_engine)
|
||||
dynamic_anim.color_source = mock_color_source
|
||||
dynamic_anim.wave_period = 1000
|
||||
dynamic_anim.wave_length = 3
|
||||
dynamic_anim.shift_period = 1000
|
||||
dynamic_anim.spatial_period = 3
|
||||
|
||||
# Start the animation
|
||||
dynamic_anim.start()
|
||||
@ -217,34 +153,22 @@ frame.clear()
|
||||
result = dynamic_anim.render(frame, mock_engine.time_ms)
|
||||
assert(result, "Render should return true")
|
||||
|
||||
# Test 6: Parameter validation
|
||||
print("Test 6: Parameter validation")
|
||||
var validation_anim = animation.palette_wave_animation(mock_engine)
|
||||
# Test 4: Parameter validation
|
||||
print("Test 4: Parameter validation")
|
||||
var validation_anim = animation.palette_gradient_animation(mock_engine)
|
||||
|
||||
# Test valid parameter values
|
||||
validation_anim.wave_period = 500
|
||||
assert(validation_anim.wave_period == 500, "Valid wave period should be accepted")
|
||||
validation_anim.shift_period = 500
|
||||
assert(validation_anim.shift_period == 500, "Valid shift period should be accepted")
|
||||
|
||||
validation_anim.wave_length = 1
|
||||
assert(validation_anim.wave_length == 1, "Valid wave length should be accepted")
|
||||
validation_anim.spatial_period = 1
|
||||
assert(validation_anim.spatial_period == 1, "Valid spatial period should be accepted")
|
||||
|
||||
# Test invalid parameter values (should be constrained by min values)
|
||||
try
|
||||
validation_anim.wave_period = 0 # Below minimum
|
||||
assert(false, "Should not accept wave_period below minimum")
|
||||
except .. as e
|
||||
# Expected to fail validation
|
||||
end
|
||||
validation_anim.phase_shift = 128
|
||||
assert(validation_anim.phase_shift == 128, "Valid phase shift should be accepted")
|
||||
|
||||
try
|
||||
validation_anim.wave_length = 0 # Below minimum
|
||||
assert(false, "Should not accept wave_length below minimum")
|
||||
except .. as e
|
||||
# Expected to fail validation
|
||||
end
|
||||
|
||||
# Test 7: Animation with different color mapping
|
||||
print("Test 7: Animation with different color mapping")
|
||||
# Test 5: Animation with different color mapping
|
||||
print("Test 5: Animation with different color mapping")
|
||||
class MockRainbowColorSource
|
||||
def get_color_for_value(value, time_ms)
|
||||
# Simple rainbow mapping based on value (expecting 0-255 range)
|
||||
@ -274,19 +198,22 @@ frame.clear()
|
||||
result = rich_anim.render(frame, mock_engine.time_ms)
|
||||
assert(result, "Render should return true")
|
||||
|
||||
# Test 8: Animation timing and synchronization
|
||||
print("Test 8: Animation timing and synchronization")
|
||||
# Test 6: Animation timing and synchronization
|
||||
print("Test 6: Animation timing and synchronization")
|
||||
var sync_time = mock_engine.time_ms + 1000
|
||||
|
||||
# Create multiple animations
|
||||
var anim1 = animation.palette_wave_animation(mock_engine)
|
||||
var anim1 = animation.palette_gradient_animation(mock_engine)
|
||||
anim1.color_source = mock_color_source
|
||||
anim1.wave_period = 1000
|
||||
anim1.wave_length = 4
|
||||
anim1.shift_period = 1000
|
||||
anim1.spatial_period = 4
|
||||
|
||||
var anim2 = animation.palette_gradient_animation(mock_engine)
|
||||
var anim2 = animation.palette_meter_animation(mock_engine)
|
||||
anim2.color_source = mock_color_source2
|
||||
anim2.shift_period = 1500
|
||||
def meter_func(engine, time_ms, animation)
|
||||
return 128
|
||||
end
|
||||
anim2.value_func = meter_func
|
||||
|
||||
# Start both animations at the same time
|
||||
anim1.start(sync_time)
|
||||
@ -297,11 +224,11 @@ anim2.update(sync_time) # force first tick
|
||||
assert(anim1.start_time == sync_time, "Animation 1 should have correct start time")
|
||||
assert(anim2.start_time == sync_time, "Animation 2 should have correct start time")
|
||||
|
||||
# Test 9: Animation without color source (should handle gracefully)
|
||||
print("Test 9: Animation without color source")
|
||||
var no_color_anim = animation.palette_wave_animation(mock_engine)
|
||||
no_color_anim.wave_period = 1000
|
||||
no_color_anim.wave_length = 3
|
||||
# Test 7: Animation without color source (should handle gracefully)
|
||||
print("Test 7: Animation without color source")
|
||||
var no_color_anim = animation.palette_gradient_animation(mock_engine)
|
||||
no_color_anim.shift_period = 1000
|
||||
no_color_anim.spatial_period = 3
|
||||
# Note: no color_source set
|
||||
|
||||
no_color_anim.start()
|
||||
@ -310,9 +237,9 @@ frame.clear()
|
||||
result = no_color_anim.render(frame, mock_engine.time_ms)
|
||||
assert(!result, "Render should return false when no color source is set")
|
||||
|
||||
# Test 10: String representation
|
||||
print("Test 10: String representation")
|
||||
var str_anim = animation.palette_wave_animation(mock_engine)
|
||||
# Test 8: String representation
|
||||
print("Test 8: String representation")
|
||||
var str_anim = animation.palette_gradient_animation(mock_engine)
|
||||
var str_repr = str_anim.tostring()
|
||||
print(f"String representation: {str_repr}")
|
||||
assert(str_repr != nil, "String representation should not be nil")
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
.gitignore
|
||||
vsc-extension-quickstart.md
|
||||
**/node_modules/**
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
@ -0,0 +1,87 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to the Animation DSL extension will be documented in this file.
|
||||
|
||||
## [1.2.0] - 2025-01-24
|
||||
|
||||
### Added
|
||||
- Support for `if` keyword for conditional execution in sequences
|
||||
- Conditional execution allows boolean-based gating (runs 0 or 1 times)
|
||||
|
||||
### Changed
|
||||
- Updated keyword patterns to include `if` statement
|
||||
|
||||
## [1.1.0] - 2025-01-09
|
||||
|
||||
### Added
|
||||
- Support for `import` statements for Berry modules
|
||||
- Support for `template` and `param` keywords for template definitions
|
||||
- Support for `type` annotations in template parameters (`param name type color`)
|
||||
- Support for `set` keyword for variable assignments
|
||||
- Support for `reset` and `restart` keywords for value provider/animation control
|
||||
- Support for `log` keyword for debug logging
|
||||
- Support for `as` keyword in import and parameter declarations
|
||||
- Mathematical functions: `abs`, `max`, `min`, `round`, `sqrt`, `scale`, `sin`, `cos`
|
||||
- User function syntax: `user.function_name()` with proper highlighting
|
||||
- Additional animation functions: `pulsating_animation`
|
||||
- Additional value providers: `triangle`, `cosine_osc`, `sawtooth`, `color_cycle`, `strip_length`
|
||||
- Additional easing types: `triangle`, `sine`, `sawtooth`, `elastic`, `bounce`
|
||||
- Support for hexadecimal colors with `0x` prefix (e.g., `0xFF0000`, `0x80FF0000`)
|
||||
- Additional animation properties: `opacity`, `priority`, `pos`, `beacon_size`, `slew_size`, `direction`, `tail_length`, `speed`, `period`, `cycle_period`, `min_value`, `max_value`, `duration`, `next`
|
||||
|
||||
### Changed
|
||||
- Updated keyword patterns to match current DSL syntax based on actual examples
|
||||
- Improved indentation rules to support `template` blocks with proper `{}`
|
||||
- Enhanced color recognition to support both `#RRGGBB` and `0xRRGGBB` formats
|
||||
- Expanded animation function list to include all currently implemented functions
|
||||
- Updated oscillator functions to match actual DSL implementation
|
||||
|
||||
### Fixed
|
||||
- Corrected animation function names to match actual DSL implementation
|
||||
- Updated oscillator function patterns to include all available value providers
|
||||
- Fixed keyword list to remove deprecated/unused keywords and add missing ones
|
||||
|
||||
## [1.0.0] - 2024-01-30
|
||||
|
||||
### Added
|
||||
- Initial release of Animation DSL syntax highlighting
|
||||
- Complete syntax highlighting for all DSL constructs:
|
||||
- Keywords (strip, color, palette, animation, sequence, etc.)
|
||||
- Animation functions (solid, rich_palette_animation, beacon_animation, etc.)
|
||||
- Oscillator functions (ramp, linear, smooth, square)
|
||||
- Colors (hex colors and 30+ named colors)
|
||||
- Time literals (ms, s, m, h)
|
||||
- Percentages (50%, 100%)
|
||||
- Comments (# line comments)
|
||||
- Semantic token scopes that work with any VSCode theme
|
||||
- Language configuration with:
|
||||
- Auto-closing pairs for brackets and quotes
|
||||
- Comment toggling support
|
||||
- Smart indentation for sequences and loops
|
||||
- Bracket matching
|
||||
- File association for .anim files
|
||||
- Comprehensive documentation and examples
|
||||
|
||||
### Features
|
||||
- **Syntax Highlighting**: Full coverage of Animation DSL grammar
|
||||
- **Theme Compatibility**: Works with any VSCode theme (dark, light, high contrast)
|
||||
- **Language Features**: Auto-closing, indentation, bracket matching
|
||||
- **Documentation**: Complete README with examples and usage guide
|
||||
|
||||
### Supported DSL Features
|
||||
- Strip configuration (`strip length 60`)
|
||||
- Color definitions (`color red = #FF0000`)
|
||||
- Palette definitions with VRGB format
|
||||
- Animation definitions (8 animation functions)
|
||||
- Value providers (4 oscillator functions)
|
||||
- Property assignments (`animation.priority = 10`)
|
||||
- Sequences with play, wait, and repeat
|
||||
- Comments with preservation
|
||||
- All currently implemented DSL constructs
|
||||
|
||||
### Technical Details
|
||||
- Based on TextMate grammar for syntax highlighting
|
||||
- JSON-based snippet system with parameter placeholders
|
||||
- Custom theme with semantic color coding
|
||||
- Language configuration for VSCode integration
|
||||
- Supports all Animation DSL file extensions (.anim)
|
||||
@ -0,0 +1,139 @@
|
||||
# Animation DSL for VSCode
|
||||
|
||||
This extension provides syntax highlighting, snippets, and language support for the Berry Animation Framework DSL (.anim files).
|
||||
|
||||
## Features
|
||||
|
||||
### Syntax Highlighting
|
||||
- **Keywords**: `strip`, `color`, `palette`, `animation`, `sequence`, `template`, `import`, `set`, `play`, `run`, `if`, `repeat`, `reset`, `restart`, `log`, etc.
|
||||
- **Animation Functions**: `solid`, `pulsating_animation`, `rich_palette_animation`, `beacon_animation`, `comet_animation`, etc.
|
||||
- **Value Providers**: `triangle`, `cosine_osc`, `sawtooth`, `color_cycle`, `strip_length`, `ramp`, `linear`, `smooth`, `square`
|
||||
- **Mathematical Functions**: `abs`, `max`, `min`, `round`, `sqrt`, `scale`, `sin`, `cos`
|
||||
- **User Functions**: `user.function_name()` syntax with proper highlighting
|
||||
- **Colors**: Hex colors (`#FF0000`, `0xFF0000`, `0x80FF0000`) and named colors (red, blue, etc.)
|
||||
- **Time Literals**: 2s, 500ms, 1m, 2h
|
||||
- **Percentages**: 50%, 100%
|
||||
- **Comments**: Line comments starting with #
|
||||
- **Template Syntax**: Template definitions with parameter type annotations
|
||||
|
||||
### Language Features
|
||||
- **Auto-closing**: Brackets, quotes, and braces
|
||||
- **Comment toggling**: Ctrl+/ for line comments
|
||||
- **Bracket matching**: Matching brackets and braces
|
||||
- **Indentation**: Smart indentation for sequences and loops
|
||||
- **Theme Compatibility**: Works with any VSCode theme (dark, light, high contrast)
|
||||
|
||||
## File Association
|
||||
|
||||
This extension automatically activates for files with the `.anim` extension.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```dsl
|
||||
# Fire Effect Animation with Templates
|
||||
import user_functions
|
||||
|
||||
# Define fire palette
|
||||
palette fire_colors = [
|
||||
(0, 0x000000) # Black
|
||||
(64, 0x800000) # Dark red
|
||||
(128, 0xFF0000) # Red
|
||||
(192, 0xFF8000) # Orange
|
||||
(255, 0xFFFF00) # Yellow
|
||||
]
|
||||
|
||||
# Template for reusable fire effect
|
||||
template fire_effect {
|
||||
param base_palette type palette
|
||||
param intensity type number
|
||||
param duration
|
||||
|
||||
animation campfire = rich_palette_animation(
|
||||
palette=base_palette
|
||||
cycle_period=duration
|
||||
)
|
||||
|
||||
# Use computed values and user functions
|
||||
campfire.priority = max(5, intensity / 10)
|
||||
campfire.opacity = user.breathing_effect()
|
||||
|
||||
run campfire
|
||||
}
|
||||
|
||||
# Use the template
|
||||
fire_effect(fire_colors, 200, 3s)
|
||||
|
||||
# Sequence with dynamic property changes
|
||||
sequence fire_demo {
|
||||
play campfire for 5s
|
||||
campfire.opacity = abs(strip_length() * 4)
|
||||
play campfire for 3s
|
||||
reset campfire
|
||||
play campfire for 2s
|
||||
}
|
||||
|
||||
run fire_demo
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Theme Compatibility
|
||||
|
||||
The extension uses semantic token scopes that work with any VSCode theme:
|
||||
- **Keywords**: Uses standard keyword colors from your chosen theme
|
||||
- **Functions**: Uses function colors from your chosen theme
|
||||
- **Constants**: Uses constant colors from your chosen theme (colors, numbers, etc.)
|
||||
- **Comments**: Uses comment colors from your chosen theme
|
||||
- **Strings**: Uses string colors from your chosen theme
|
||||
|
||||
This ensures the syntax highlighting looks great whether you prefer dark themes, light themes, or high contrast themes.
|
||||
|
||||
## Installation
|
||||
|
||||
### From VSIX (Recommended)
|
||||
1. Download the `.vsix` file
|
||||
2. Open VSCode
|
||||
3. Go to Extensions (Ctrl+Shift+X)
|
||||
4. Click the "..." menu and select "Install from VSIX..."
|
||||
5. Select the downloaded `.vsix` file
|
||||
|
||||
### Manual Installation
|
||||
1. Copy the extension folder to your VSCode extensions directory:
|
||||
- **Windows**: `%USERPROFILE%\.vscode\extensions\`
|
||||
- **macOS**: `~/.vscode/extensions/`
|
||||
- **Linux**: `~/.vscode/extensions/`
|
||||
2. Restart VSCode
|
||||
|
||||
## Development
|
||||
|
||||
### Building the Extension
|
||||
```bash
|
||||
npm install -g @vscode/vsce
|
||||
cd vscode-animation-dsl
|
||||
vsce package
|
||||
```
|
||||
|
||||
This creates a `.vsix` file that can be installed in VSCode.
|
||||
|
||||
### Testing
|
||||
1. Open the extension folder in VSCode
|
||||
2. Press F5 to launch a new Extension Development Host
|
||||
3. Open a `.anim` file to test syntax highlighting
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||
|
||||
### Adding New Features
|
||||
- **Keywords**: Add to `syntaxes/animation-dsl.tmLanguage.json`
|
||||
- **Token Scopes**: Modify semantic scopes in the grammar file
|
||||
|
||||
## License
|
||||
|
||||
This extension is part of the Berry Animation Framework project and follows the same license terms.
|
||||
|
||||
## Related
|
||||
|
||||
- [Berry Animation Framework](https://github.com/tasmota/berry-animation-framework)
|
||||
- [Tasmota](https://tasmota.github.io/docs/)
|
||||
- [Berry Language](https://github.com/berry-lang/berry)
|
||||
@ -0,0 +1,81 @@
|
||||
{
|
||||
"comments":
|
||||
{
|
||||
"lineComment": "#"
|
||||
},
|
||||
"brackets":
|
||||
[
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
]
|
||||
],
|
||||
"autoClosingPairs":
|
||||
[
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"\"",
|
||||
"\""
|
||||
],
|
||||
[
|
||||
"'",
|
||||
"'"
|
||||
]
|
||||
],
|
||||
"surroundingPairs":
|
||||
[
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"\"",
|
||||
"\""
|
||||
],
|
||||
[
|
||||
"'",
|
||||
"'"
|
||||
]
|
||||
],
|
||||
"folding":
|
||||
{
|
||||
"markers":
|
||||
{
|
||||
"start": "^\\s*#\\s*region\\b",
|
||||
"end": "^\\s*#\\s*endregion\\b"
|
||||
}
|
||||
},
|
||||
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
|
||||
"indentationRules":
|
||||
{
|
||||
"increaseIndentPattern": "^\\s*(sequence|repeat|template)\\s+.*\\{\\s*$",
|
||||
"decreaseIndentPattern": "^\\s*}\\s*$"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "animation-dsl",
|
||||
"displayName": "Animation DSL",
|
||||
"description": "Syntax highlighting for Berry Animation Framework DSL (.anim files)",
|
||||
"version": "1.2.1",
|
||||
"publisher": "tasmota",
|
||||
"engines": {
|
||||
"vscode": "^1.74.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"keywords": [
|
||||
"animation",
|
||||
"led",
|
||||
"tasmota",
|
||||
"berry"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/arendst/Tasmota"
|
||||
},
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "animation-dsl",
|
||||
"aliases": [
|
||||
"Animation DSL",
|
||||
"animation-dsl",
|
||||
"anim"
|
||||
],
|
||||
"extensions": [
|
||||
".anim"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "animation-dsl",
|
||||
"scopeName": "source.animation-dsl",
|
||||
"path": "./syntaxes/animation-dsl.tmLanguage.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "^2.15.0"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,297 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "Animation DSL",
|
||||
"scopeName": "source.animation-dsl",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"include": "#numbers"
|
||||
},
|
||||
{
|
||||
"include": "#colors"
|
||||
},
|
||||
{
|
||||
"include": "#time-literals"
|
||||
},
|
||||
{
|
||||
"include": "#percentages"
|
||||
},
|
||||
{
|
||||
"include": "#animation-functions"
|
||||
},
|
||||
{
|
||||
"include": "#oscillator-functions"
|
||||
},
|
||||
{
|
||||
"include": "#named-colors"
|
||||
},
|
||||
{
|
||||
"include": "#easing-types"
|
||||
},
|
||||
{
|
||||
"include": "#mathematical-functions"
|
||||
},
|
||||
{
|
||||
"include": "#user-functions"
|
||||
},
|
||||
{
|
||||
"include": "#operators"
|
||||
},
|
||||
{
|
||||
"include": "#identifiers"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.line.number-sign.animation-dsl",
|
||||
"begin": "#",
|
||||
"end": "$",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.animation-dsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.animation-dsl",
|
||||
"match": "\\b(strip|set|import|berry|extern|color|palette|animation|sequence|function|template|param|type|play|for|with|repeat|times|forever|if|else|elif|choose|random|on|run|wait|goto|interrupt|resume|while|from|to|return|reset|restart|every)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.other.animation-dsl",
|
||||
"match": "\\b(opacity|priority|pos|beacon_size|slew_size|direction|tail_length|speed|period|cycle_period|min_value|max_value|duration|next)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.language.boolean.animation-dsl",
|
||||
"match": "\\b(true|false|nil|transparent)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.other.event.animation-dsl",
|
||||
"match": "\\b(startup|shutdown|button_press|button_hold|motion_detected|brightness_change|timer|time|sound_peak|network_message)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.animation-dsl",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.animation-dsl",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.animation-dsl",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.animation-dsl",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"numbers": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.float.animation-dsl",
|
||||
"match": "\\b\\d+\\.\\d+\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.integer.animation-dsl",
|
||||
"match": "\\b\\d+\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.other.color.hex.animation-dsl",
|
||||
"match": "0x[0-9A-Fa-f]{6}\\b",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "constant.other.color.hex.rgb.animation-dsl"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constant.other.color.hex.animation-dsl",
|
||||
"match": "0x[0-9A-Fa-f]{8}\\b",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "constant.other.color.hex.argb.animation-dsl"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constant.other.color.hex.animation-dsl",
|
||||
"match": "#[0-9A-Fa-f]{6}\\b",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "constant.other.color.hex.rgb.animation-dsl"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constant.other.color.hex.animation-dsl",
|
||||
"match": "#[0-9A-Fa-f]{8}\\b",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "constant.other.color.hex.argb.animation-dsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"time-literals": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.time.animation-dsl",
|
||||
"match": "\\b\\d+(?:\\.\\d+)?\\s*(ms|s|m|h)\\b",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "keyword.other.unit.time.animation-dsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"percentages": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.percentage.animation-dsl",
|
||||
"match": "\\b\\d+(?:\\.\\d+)?%\\b",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "constant.numeric.percentage.animation-dsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"animation-functions": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "entity.name.function.animation.animation-dsl",
|
||||
"match": "\\b(solid|pulsating_animation|beacon_animation|comet_animation|rich_palette_animation|twinkle_animation|breathe_animation|fire_animation|crenel_position_animation)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oscillator-functions": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "entity.name.function.oscillator.animation-dsl",
|
||||
"match": "\\b(triangle|cosine_osc|sawtooth|ramp|linear|smooth|square|sine|color_cycle|strip_length)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"named-colors": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.other.color.named.primary.animation-dsl",
|
||||
"match": "\\b(red|green|blue|white|black|yellow|orange|purple|pink|cyan|magenta)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.other.color.named.extended.animation-dsl",
|
||||
"match": "\\b(gray|grey|silver|gold|brown|lime|navy|olive|maroon|teal|aqua|fuchsia|indigo|violet|crimson|coral|salmon|khaki|plum|orchid|turquoise|tan|beige|ivory|snow|transparent)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"easing-types": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.other.easing.animation-dsl",
|
||||
"match": "\\b(linear|triangle|smooth|sine|ease_in|ease_out|ramp|sawtooth|square|elastic|bounce)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mathematical-functions": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "entity.name.function.math.animation-dsl",
|
||||
"match": "\\b(abs|max|min|round|sqrt|scale|sin|cos)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"user-functions": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "entity.name.function.user.animation-dsl",
|
||||
"match": "\\buser\\.[a-zA-Z_][a-zA-Z0-9_]*\\b",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "entity.name.function.user.animation-dsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.assignment.animation-dsl",
|
||||
"match": "="
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.animation-dsl",
|
||||
"match": "[+\\-*/%^]"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.animation-dsl",
|
||||
"match": "(==|!=|<=|>=|<|>)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.animation-dsl",
|
||||
"match": "(&&|\\|\\||!)"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.separator.comma.animation-dsl",
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "punctuation.separator.colon.animation-dsl",
|
||||
"match": ":"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.separator.dot.animation-dsl",
|
||||
"match": "\\."
|
||||
},
|
||||
{
|
||||
"name": "punctuation.section.brackets.begin.animation-dsl",
|
||||
"match": "[\\[\\(\\{]"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.section.brackets.end.animation-dsl",
|
||||
"match": "[\\]\\)\\}]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"identifiers": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "variable.other.animation-dsl",
|
||||
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user