festoon.yaml
substitutions:
pin_pixels: GPIO2
pin_pixel_power: GPIO13
pixel_type: RGB
num_leds: "100" # Don't go more than 255, as that's the current limit of the algorithm.
# Increasing the internal canvas beyond 255 is buggy, likely a misuse of 8 bit int somewhere.
update_interval: 10ms
neopixel_method: ESP32_I2S
esphome:
on_shutdown:
then:
- light.turn_off: # Turn it off to make OTA flashing more reliable
id: the_pixels
globals:
- id: global_ambient_r
type: int
restore_value: no
initial_value: "0"
- id: global_ambient_g
type: int
restore_value: no
initial_value: "0"
- id: global_ambient_b
type: int
restore_value: no
initial_value: "0"
- id: global_colour_volume
type: int
restore_value: no
initial_value: "0"
- id: global_saturation
type: int
restore_value: no
initial_value: "0"
- id: global_saturationf
type: float
restore_value: no
initial_value: "0"
- id: global_warmth
type: int
restore_value: no
initial_value: "0"
- id: global_sparkle_power
type: int
restore_value: no
initial_value: "0"
- id: global_sparkle_rate
type: int
restore_value: no
initial_value: "0"
- id: global_shimmer
type: int
restore_value: no
initial_value: "0"
output:
- platform: template
id: ambient_r_out
type: float
write_action:
then:
- lambda: |-
id(global_ambient_r) = state*255;
ESP_LOGI("Ambient R", "%f > %d", state, id(global_ambient_r));
- platform: template
id: ambient_g_out
type: float
write_action:
then:
- lambda: |-
id(global_ambient_g) = state*255;
ESP_LOGI("Ambient G", "%f > %d", state, id(global_ambient_g));
- platform: template
id: ambient_b_out
type: float
write_action:
then:
- lambda: |-
id(global_ambient_b) = state*255;
ESP_LOGI("Ambient B", "%f > %d", state, id(global_ambient_b));
- platform: template
id: colour_volume_out
type: float
write_action:
then:
- lambda: |-
id(global_colour_volume) = state*45;
- platform: template
id: saturation_out
type: float
write_action:
then:
- lambda: |-
id(global_saturation) = state*255;
id(global_saturationf) = state;
- platform: template
id: warmth_out
type: float
write_action:
then:
- lambda: |-
id(global_warmth) = state*50;
- platform: template
id: sparkle_power_out
type: float
write_action:
then:
- lambda: |-
id(global_sparkle_power) = state*255;
- platform: template
id: sparkle_rate_out
type: float
write_action:
then:
- lambda: |-
id(global_sparkle_rate) = state*22;
- platform: template
id: shimmer_out
type: float
write_action:
then:
- lambda: |-
id(global_shimmer) = state*20;
power_supply:
- id: pixel_relay
pin:
number: ${pin_pixel_power}
inverted: "true"
keep_on_time: 100ms
light:
- platform: rgb
name: "${entity_prefix} Ambient"
id: ambient_light
red: ambient_r_out
green: ambient_g_out
blue: ambient_b_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: monochromatic
name: "${entity_prefix} Colour Volume"
id: colour_volume_light
output: colour_volume_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: monochromatic
name: "${entity_prefix} Saturation"
id: saturation_light
output: saturation_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: monochromatic
name: "${entity_prefix} Warmth"
id: warmth_light
output: warmth_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: monochromatic
name: "${entity_prefix} Sparkle Brightness"
id: sparkle_power_light
output: sparkle_power_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: monochromatic
name: "${entity_prefix} Sparkle Quantity"
id: sparkle_rate_light
output: sparkle_rate_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: monochromatic
name: "${entity_prefix} Shimmer"
id: shimmer_light
output: shimmer_out
gamma_correct: 1
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_OFF
- platform: neopixelbus
id: the_pixels
name: "${entity_prefix}"
type: ${pixel_type}
variant: 800KBPS
method: ${neopixel_method}
power_supply: pixel_relay
pin: ${pin_pixels}
num_leds: ${num_leds}
restore_mode: RESTORE_DEFAULT_OFF
effects:
- addressable_lambda:
name: "Festoon"
update_interval: ${update_interval}
lambda: |-
// Static constants
static const int stroke_length = 15; // Longer strokes increases accumulation per iteration
static const int canvas_size = 255; // Bigger canvas disperses accumulation per iteration
static const int canvas_offset = (canvas_size - it.size()) / 2;
static const uint8_t red_paint_hue = 15; // 0 is pure red
static const uint8_t amber_paint_hue = 30; // 32 is orange
static const uint8_t yellow_paint_hue = 60; // 64 is pure yellow
static const uint8_t teal_paint_hue = 150; // 128 is pure aqua
static const uint8_t ambient_hue = 200; // 160 is pure blue; was set to 180 for a while
// Static variables
static int red_brush[canvas_size];
static int red_heat[canvas_size];
static int amber_brush[canvas_size];
static int amber_heat[canvas_size];
static int yellow_brush[canvas_size];
static int yellow_heat[canvas_size];
static int teal_brush[canvas_size];
static int teal_heat[canvas_size];
// Iteration counter
static uint32_t effect_i;
effect_i++;
if (effect_i >= 6000) effect_i = 0;
//ESP_LOGI("Iteration", "%i", effect_i);
// Esphome global values
esphome::Color ambient_colour = esphome::Color(id(global_ambient_r), id(global_ambient_g), id(global_ambient_b));
int ambient_level = max(id(global_ambient_r), max(id(global_ambient_g), id(global_ambient_b)));
int colour_volume = id(global_colour_volume);
int saturation = id(global_saturation);
float sat_float = id(global_saturationf);
float sat_mult = (sat_float/2) + 0.5;
int warmth = id(global_warmth);
int sparkle_power = id(global_sparkle_power);
int sparkle_rate = id(global_sparkle_rate);
int shimmer = id(global_shimmer);
if (initial_run) {
effect_i = 0;
memset(red_brush, 0, canvas_size);
memset(red_heat, 0, canvas_size);
memset(amber_brush, 0, canvas_size);
memset(amber_heat, 0, canvas_size);
memset(yellow_brush,0, canvas_size);
memset(yellow_heat, 0, canvas_size);
memset(teal_brush, 0, canvas_size);
memset(teal_heat, 0, canvas_size);
}
// Inject a new brush in a random location every so often
if (effect_i % 7 == 0) red_brush[random_uint32() % canvas_size] = 1;
if (effect_i % 11 == 0) amber_brush[random_uint32() % canvas_size] = 1;
if (effect_i % 10 == 0) yellow_brush[random_uint32() % canvas_size] = 1;
if (effect_i % 8 == 0) teal_brush[random_uint32() % canvas_size] = 1;
// Every second iteration, shift brushes along
if (effect_i % 2 == 0) {
// Swipe red and yellow brushes forwards by iterating backwards
for ( int i = canvas_size; i >= 0; i--) {
if (red_brush[i] > 0) {
if (red_brush[i] >= stroke_length) red_brush[i] = 0; // When the brush has iterated enough times, delete it
else if ((i+1) < canvas_size) red_brush[i+1] = red_brush[i]+1; // Increment and move to next position in array
red_brush[i] = 0; // Zero this position
}
if (yellow_brush[i] > 0) {
if (yellow_brush[i] >= stroke_length) yellow_brush[i] = 0;
else if ((i+1) < canvas_size) yellow_brush[i+1] = yellow_brush[i]+1;
yellow_brush[i] = 0;
}
}
// Swipe amber and teal brushes backwards by iterating forwards
for ( int i = 0; i < canvas_size; i++) {
if (amber_brush[i] > 0) {
if (amber_brush[i] >= stroke_length) amber_brush[i] = 0;
else if (i > 0) amber_brush[i-1] = amber_brush[i]+1;
amber_brush[i] = 0;
}
if (teal_brush[i] > 0) {
if (teal_brush[i] >= stroke_length) teal_brush[i] = 0;
else if (i > 0) teal_brush[i-1] = teal_brush[i]+1;
teal_brush[i] = 0;
}
}
}
// Set up a pixel canvas
Color pcanvas[canvas_size];
// For each position on the canvas
for ( int i = 0; i < canvas_size; i++) {
// Continuously cool down
if (red_heat[i] > 0) red_heat[i]--;
if (amber_heat[i] > 0) amber_heat[i]--;
if (yellow_heat[i] > 0) yellow_heat[i]--;
if (teal_heat[i] > 0) teal_heat[i]--;
// Cool more aggressively when extra hot
if (red_heat[i] > 350) red_heat[i]--;
if (amber_heat[i] > 350) amber_heat[i]--;
if (yellow_heat[i] > 350) yellow_heat[i]--;
if (teal_heat[i] > 350) teal_heat[i]--;
// Apply brush to heatmap
if (red_brush[i] > 0) red_heat[i] += ((stroke_length - red_brush[i])*2) + colour_volume;
if (amber_brush[i] > 0) amber_heat[i] += ((stroke_length - amber_brush[i])*2) + colour_volume;
if (yellow_brush[i] > 0) yellow_heat[i] += ((stroke_length - yellow_brush[i])*2) + colour_volume;
if (teal_brush[i] > 0) teal_heat[i] += ((stroke_length - teal_brush[i])*2) + colour_volume;
// If any values exceed 255, accumulate the overflow(s) and add it back as (warm) white later
// This means an overly bright red (for example) won't clip at pure red, but rather keep getting brigher towards pink and ultimately pure white
int overflow_heat = std::min(255,
( red_heat[i]>255 ? red_heat[i]-255 : 0 )
+ ( amber_heat[i]>255 ? amber_heat[i]-255 : 0 )
+ ( yellow_heat[i]>255 ? yellow_heat[i]-255 : 0 )
+ ( teal_heat[i]>255 ? teal_heat[i]-255 : 0 )
);
// Calculate colour as HSV, convert to RGB, additively mix to pixel
pcanvas[i] = Color::BLACK
+ ESPHSVColor(red_paint_hue, saturation, std::min(255, (int)( red_heat[i] * sat_mult ))).to_rgb()
+ ESPHSVColor(amber_paint_hue, saturation, std::min(255, (int)( amber_heat[i] * sat_mult ))).to_rgb()
+ ESPHSVColor(yellow_paint_hue, saturation, std::min(255, (int)( yellow_heat[i] * sat_mult ))).to_rgb()
+ ESPHSVColor(teal_paint_hue, saturation/2, std::min(255, (int)( teal_heat[i] * sat_mult ))).to_rgb()
+ Color(overflow_heat, overflow_heat/2, overflow_heat/2)
- Color(0, warmth/2, warmth);
// Add ambience (i.e. a solid base colour)
// But only when the pixel was going to be dark
int intensity = (pcanvas[i].r + pcanvas[i].g + pcanvas[i].b)/2;
if (intensity < ambient_level) {
pcanvas[i] += ambient_colour.darken(intensity);
}
// Add shimmer (i.e. random noise)
if (shimmer > 0) {
if (random_uint32() % 2 == 0) {
pcanvas[i] += Color(
(random_uint32() % shimmer),
(random_uint32() % shimmer),
(random_uint32() % shimmer)
);
} else {
pcanvas[i] -= Color(
(random_uint32() % shimmer),
(random_uint32() % shimmer),
(random_uint32() % shimmer)
);
}
}
}
// Sparkle (i.e. random bright pixels)
if (sparkle_rate > 0 && sparkle_power > 0) {
// If sparkle power is low, randomly sparkle between 0 and 1 pixels per iteration
if (sparkle_rate < 15) {
int rollTheDice = random_uint32() % 15;
if (sparkle_rate > rollTheDice) {
size_t pickAPixel = random_uint32() % canvas_size;
pcanvas[pickAPixel] += ESPHSVColor(0, 0, sparkle_power).to_rgb();
}
// If sparkle power is high, randomly sparkle multiple pixels per iteration
} else if (sparkle_rate >= 15) {
for (int i = 15; i <= sparkle_rate; i++) {
size_t pickAPixel = random_uint32() % canvas_size;
pcanvas[pickAPixel] += ESPHSVColor(0, 0, sparkle_power).to_rgb();
}
}
}
// Copy values from canvas (pcanvas) to Esphome pixels (supplied by lambda as "it")
for ( int i = 0; i < it.size(); i++) {
it[i] = pcanvas[i+canvas_offset]; // Offset means it's copying values from the centre of the canvas
}