sTodo-m5paper-client/libraries/FastLED/examples/FxWave2d/wavefx.cpp
2025-06-30 21:41:49 +02:00

401 lines
18 KiB
C++

/*
This demo is best viewed using the FastLED compiler.
Windows/MacOS binaries: https://github.com/FastLED/FastLED/releases
Python
Install: pip install fastled
Run: fastled <this sketch directory>
This will compile and preview the sketch in the browser, and enable
all the UI elements you see below.
OVERVIEW:
This sketch demonstrates a 2D wave simulation with multiple layers and blending effects.
It creates ripple effects that propagate across the LED matrix, similar to water waves.
The demo includes two wave layers (upper and lower) with different colors and properties,
which are blended together to create complex visual effects.
*/
#include <Arduino.h> // Core Arduino functionality
#include <FastLED.h> // Main FastLED library for controlling LEDs
#include "fl/math_macros.h" // Math helper functions and macros
#include "fl/time_alpha.h" // Time-based alpha/transition effects
#include "fl/ui.h" // UI components for the FastLED web compiler
#include "fx/2d/blend.h" // 2D blending effects between layers
#include "fx/2d/wave.h" // 2D wave simulation
#include "wavefx.h" // Header file for this sketch
using namespace fl; // Use the FastLED namespace for convenience
// Array to hold all LED color values - one CRGB struct per LED
CRGB leds[NUM_LEDS];
// UI elements that appear in the FastLED web compiler interface:
UITitle title("FxWave2D Demo");
UIDescription description("Advanced layered and blended wave effects.");
UICheckbox xCyclical("X Is Cyclical", false); // If true, waves wrap around the x-axis (like a loop)
// Main control UI elements:
UIButton button("Trigger"); // Button to trigger a single ripple
UIButton buttonFancy("Trigger Fancy"); // Button to trigger a fancy cross-shaped effect
UICheckbox autoTrigger("Auto Trigger", true); // Enable/disable automatic ripple triggering
UISlider triggerSpeed("Trigger Speed", .5f, 0.0f, 1.0f, 0.01f); // Controls how frequently auto-triggers happen (lower = faster)
UICheckbox easeModeSqrt("Ease Mode Sqrt", false); // Changes how wave heights are calculated (sqrt gives more natural waves)
UISlider blurAmount("Global Blur Amount", 0, 0, 172, 1); // Controls overall blur amount for all layers
UISlider blurPasses("Global Blur Passes", 1, 1, 10, 1); // Controls how many times blur is applied (more = smoother but slower)
UISlider superSample("SuperSampleExponent", 1.f, 0.f, 3.f, 1.f); // Controls anti-aliasing quality (higher = better quality but more CPU)
// Upper wave layer controls:
UISlider speedUpper("Wave Upper: Speed", 0.12f, 0.0f, 1.0f); // How fast the upper wave propagates
UISlider dampeningUpper("Wave Upper: Dampening", 8.9f, 0.0f, 20.0f, 0.1f); // How quickly the upper wave loses energy
UICheckbox halfDuplexUpper("Wave Upper: Half Duplex", true); // If true, waves only go positive (not negative)
UISlider blurAmountUpper("Wave Upper: Blur Amount", 95, 0, 172, 1); // Blur amount for upper wave layer
UISlider blurPassesUpper("Wave Upper: Blur Passes", 1, 1, 10, 1); // Blur passes for upper wave layer
// Lower wave layer controls:
UISlider speedLower("Wave Lower: Speed", 0.26f, 0.0f, 1.0f); // How fast the lower wave propagates
UISlider dampeningLower("Wave Lower: Dampening", 9.0f, 0.0f, 20.0f, 0.1f); // How quickly the lower wave loses energy
UICheckbox halfDuplexLower("Wave Lower: Half Duplex", true); // If true, waves only go positive (not negative)
UISlider blurAmountLower("Wave Lower: Blur Amount", 0, 0, 172, 1); // Blur amount for lower wave layer
UISlider blurPassesLower("Wave Lower: Blur Passes", 1, 1, 10, 1); // Blur passes for lower wave layer
// Fancy effect controls (for the cross-shaped effect):
UISlider fancySpeed("Fancy Speed", 796, 0, 1000, 1); // Speed of the fancy effect animation
UISlider fancyIntensity("Fancy Intensity", 32, 1, 255, 1); // Intensity/height of the fancy effect waves
UISlider fancyParticleSpan("Fancy Particle Span", 0.06f, 0.01f, 0.2f, 0.01f); // Width of the fancy effect lines
// Color palettes define the gradient of colors used for the wave effects
// Each entry has the format: position (0-255), R, G, B
DEFINE_GRADIENT_PALETTE(electricBlueFirePal){
0, 0, 0, 0, // Black (lowest wave height)
32, 0, 0, 70, // Dark blue (low wave height)
128, 20, 57, 255, // Electric blue (medium wave height)
255, 255, 255, 255 // White (maximum wave height)
};
DEFINE_GRADIENT_PALETTE(electricGreenFirePal){
0, 0, 0, 0, // Black (lowest wave height)
8, 128, 64, 64, // Green with red tint (very low wave height)
16, 255, 222, 222, // Pinkish red (low wave height)
64, 255, 255, 255, // White (medium wave height)
255, 255, 255, 255 // White (maximum wave height)
};
// Create mappings between 1D array positions and 2D x,y coordinates
XYMap xyMap(WIDTH, HEIGHT, IS_SERPINTINE); // For the actual LED output (may be serpentine)
XYMap xyRect(WIDTH, HEIGHT, false); // For the wave simulation (always rectangular grid)
// Create default configuration for the lower wave layer
WaveFx::Args CreateArgsLower() {
WaveFx::Args out;
out.factor = SuperSample::SUPER_SAMPLE_2X; // 2x supersampling for smoother waves
out.half_duplex = true; // Only positive waves (no negative values)
out.auto_updates = true; // Automatically update the simulation each frame
out.speed = 0.18f; // Wave propagation speed
out.dampening = 9.0f; // How quickly waves lose energy
out.crgbMap = WaveCrgbGradientMapPtr::New(electricBlueFirePal); // Color palette for this wave
return out;
}
// Create default configuration for the upper wave layer
WaveFx::Args CreateArgsUpper() {
WaveFx::Args out;
out.factor = SuperSample::SUPER_SAMPLE_2X; // 2x supersampling for smoother waves
out.half_duplex = true; // Only positive waves (no negative values)
out.auto_updates = true; // Automatically update the simulation each frame
out.speed = 0.25f; // Wave propagation speed (faster than lower)
out.dampening = 3.0f; // How quickly waves lose energy (less than lower)
out.crgbMap = WaveCrgbGradientMapPtr::New(electricGreenFirePal); // Color palette for this wave
return out;
}
// Create the two wave simulation layers with their default configurations
WaveFx waveFxLower(xyRect, CreateArgsLower()); // Lower/background wave layer (blue)
WaveFx waveFxUpper(xyRect, CreateArgsUpper()); // Upper/foreground wave layer (green/red)
// Create a blender that will combine the two wave layers
Blend2d fxBlend(xyMap);
// Convert the UI slider value to the appropriate SuperSample enum value
// SuperSample controls the quality of the wave simulation (higher = better quality but more CPU)
SuperSample getSuperSample() {
switch (int(superSample)) {
case 0:
return SuperSample::SUPER_SAMPLE_NONE; // No supersampling (fastest, lowest quality)
case 1:
return SuperSample::SUPER_SAMPLE_2X; // 2x supersampling (2x2 grid = 4 samples per pixel)
case 2:
return SuperSample::SUPER_SAMPLE_4X; // 4x supersampling (4x4 grid = 16 samples per pixel)
case 3:
return SuperSample::SUPER_SAMPLE_8X; // 8x supersampling (8x8 grid = 64 samples per pixel, slowest)
default:
return SuperSample::SUPER_SAMPLE_NONE; // Default fallback
}
}
// Create a ripple effect at a random position within the central area of the display
void triggerRipple() {
// Define a margin percentage to keep ripples away from the edges
float perc = .15f;
// Calculate the boundaries for the ripple (15% from each edge)
uint8_t min_x = perc * WIDTH; // Left boundary
uint8_t max_x = (1 - perc) * WIDTH; // Right boundary
uint8_t min_y = perc * HEIGHT; // Top boundary
uint8_t max_y = (1 - perc) * HEIGHT; // Bottom boundary
// Generate a random position within these boundaries
int x = random(min_x, max_x);
int y = random(min_y, max_y);
// Set a wave peak at this position in both wave layers
// The value 1.0 represents the maximum height of the wave
waveFxLower.setf(x, y, 1); // Create ripple in lower layer
waveFxUpper.setf(x, y, 1); // Create ripple in upper layer
}
// Create a fancy cross-shaped effect that expands from the center
void applyFancyEffect(uint32_t now, bool button_active) {
// Calculate the total animation duration based on the speed slider
// Higher fancySpeed value = shorter duration (faster animation)
uint32_t total =
map(fancySpeed.as<uint32_t>(), 0, fancySpeed.getMax(), 1000, 100);
// Create a static TimeRamp to manage the animation timing
// TimeRamp handles the transition from start to end over time
static TimeRamp pointTransition = TimeRamp(total, 0, 0);
// If the button is active, start/restart the animation
if (button_active) {
pointTransition.trigger(now, total, 0, 0);
}
// If the animation isn't currently active, exit early
if (!pointTransition.isActive(now)) {
// no need to draw
return;
}
// Find the center of the display
int mid_x = WIDTH / 2;
int mid_y = HEIGHT / 2;
// Calculate the maximum distance from center (half the width)
int amount = WIDTH / 2;
// Calculate the start and end coordinates for the cross
int start_x = mid_x - amount; // Leftmost point
int end_x = mid_x + amount; // Rightmost point
int start_y = mid_y - amount; // Topmost point
int end_y = mid_y + amount; // Bottommost point
// Get the current animation progress (0-255)
int curr_alpha = pointTransition.update8(now);
// Map the animation progress to the four points of the expanding cross
// As curr_alpha increases from 0 to 255, these points move from center to edges
int left_x = map(curr_alpha, 0, 255, mid_x, start_x); // Center to left
int down_y = map(curr_alpha, 0, 255, mid_y, start_y); // Center to top
int right_x = map(curr_alpha, 0, 255, mid_x, end_x); // Center to right
int up_y = map(curr_alpha, 0, 255, mid_y, end_y); // Center to bottom
// Convert the 0-255 alpha to 0.0-1.0 range
float curr_alpha_f = curr_alpha / 255.0f;
// Calculate the wave height value - starts high and decreases as animation progresses
// This makes the waves stronger at the beginning of the animation
float valuef = (1.0f - curr_alpha_f) * fancyIntensity.value() / 255.0f;
// Calculate the width of the cross lines
int span = fancyParticleSpan.value() * WIDTH;
// Add wave energy along the four expanding lines of the cross
// Each line is a horizontal or vertical span of pixels
// Left-moving horizontal line
for (int x = left_x - span; x < left_x + span; x++) {
waveFxLower.addf(x, mid_y, valuef); // Add to lower layer
waveFxUpper.addf(x, mid_y, valuef); // Add to upper layer
}
// Right-moving horizontal line
for (int x = right_x - span; x < right_x + span; x++) {
waveFxLower.addf(x, mid_y, valuef);
waveFxUpper.addf(x, mid_y, valuef);
}
// Downward-moving vertical line
for (int y = down_y - span; y < down_y + span; y++) {
waveFxLower.addf(mid_x, y, valuef);
waveFxUpper.addf(mid_x, y, valuef);
}
// Upward-moving vertical line
for (int y = up_y - span; y < up_y + span; y++) {
waveFxLower.addf(mid_x, y, valuef);
waveFxUpper.addf(mid_x, y, valuef);
}
}
// Structure to hold the state of UI buttons
struct ui_state {
bool button = false; // Regular ripple button state
bool bigButton = false; // Fancy effect button state
};
// Apply all UI settings to the wave effects and return button states
ui_state ui() {
// Set the easing function based on the checkbox
// Easing controls how wave heights are calculated:
// - LINEAR: Simple linear mapping (sharper waves)
// - SQRT: Square root mapping (more natural, rounded waves)
U8EasingFunction easeMode = easeModeSqrt
? U8EasingFunction::WAVE_U8_MODE_SQRT
: U8EasingFunction::WAVE_U8_MODE_LINEAR;
// Apply all settings from UI controls to the lower wave layer
waveFxLower.setSpeed(speedLower); // Wave propagation speed
waveFxLower.setDampening(dampeningLower); // How quickly waves lose energy
waveFxLower.setHalfDuplex(halfDuplexLower); // Whether waves can go negative
waveFxLower.setSuperSample(getSuperSample()); // Anti-aliasing quality
waveFxLower.setEasingMode(easeMode); // Wave height calculation method
// Apply all settings from UI controls to the upper wave layer
waveFxUpper.setSpeed(speedUpper); // Wave propagation speed
waveFxUpper.setDampening(dampeningUpper); // How quickly waves lose energy
waveFxUpper.setHalfDuplex(halfDuplexUpper); // Whether waves can go negative
waveFxUpper.setSuperSample(getSuperSample()); // Anti-aliasing quality
waveFxUpper.setEasingMode(easeMode); // Wave height calculation method
// Apply global blur settings to the blender
fxBlend.setGlobalBlurAmount(blurAmount); // Overall blur strength
fxBlend.setGlobalBlurPasses(blurPasses); // Number of blur passes
// Create parameter structures for each wave layer's blur settings
Blend2dParams lower_params = {
.blur_amount = blurAmountLower, // Blur amount for lower layer
.blur_passes = blurPassesLower, // Blur passes for lower layer
};
Blend2dParams upper_params = {
.blur_amount = blurAmountUpper, // Blur amount for upper layer
.blur_passes = blurPassesUpper, // Blur passes for upper layer
};
// Apply the layer-specific blur parameters
fxBlend.setParams(waveFxLower, lower_params);
fxBlend.setParams(waveFxUpper, upper_params);
// Return the current state of the UI buttons
ui_state state{
.button = button, // Regular ripple button
.bigButton = buttonFancy, // Fancy effect button
};
return state;
}
// Handle automatic triggering of ripples at random intervals
void processAutoTrigger(uint32_t now) {
// Static variable to remember when the next auto-trigger should happen
static uint32_t nextTrigger = 0;
// Calculate time until next trigger
uint32_t trigger_delta = nextTrigger - now;
// Handle timer overflow (happens after ~49 days of continuous running)
if (trigger_delta > 10000) {
// If the delta is suspiciously large, we probably rolled over
trigger_delta = 0;
}
// Only proceed if auto-trigger is enabled
if (autoTrigger) {
// Check if it's time for the next trigger
if (now >= nextTrigger) {
// Create a ripple
triggerRipple();
// Calculate the next trigger time based on the speed slider
// Invert the speed value so higher slider = faster triggers
float speed = 1.0f - triggerSpeed.value();
// Calculate min and max random intervals
// Higher speed = shorter intervals between triggers
uint32_t min_rand = 400 * speed; // Minimum interval (milliseconds)
uint32_t max_rand = 2000 * speed; // Maximum interval (milliseconds)
// Ensure min is actually less than max (handles edge cases)
uint32_t min = MIN(min_rand, max_rand);
uint32_t max = MAX(min_rand, max_rand);
// Ensure min and max aren't equal (would cause random() to crash)
if (min == max) {
max += 1;
}
// Schedule the next trigger at a random time in the future
nextTrigger = now + random(min, max);
}
}
}
void wavefx_setup() {
// Create a screen map for visualization in the FastLED web compiler
auto screenmap = xyMap.toScreenMap();
screenmap.setDiameter(.2); // Set the size of the LEDs in the visualization
// Initialize the LED strip:
// - NEOPIXEL is the LED type
// - 2 is the data pin number (for real hardware)
// - setScreenMap connects our 2D coordinate system to the 1D LED array
FastLED.addLeds<NEOPIXEL, 2>(leds, NUM_LEDS).setScreenMap(screenmap);
// Add both wave layers to the blender
// The order matters - lower layer is added first (background)
fxBlend.add(waveFxLower);
fxBlend.add(waveFxUpper);
}
void wavefx_loop() {
// The main program loop that runs continuously
// Get the current time in milliseconds
uint32_t now = millis();
// set the x cyclical
waveFxLower.setXCylindrical(xCyclical.value()); // Set whether lower wave wraps around x-axis
// Apply all UI settings and get button states
ui_state state = ui();
// Check if the regular ripple button was pressed
if (state.button) {
triggerRipple(); // Create a single ripple
}
// Apply the fancy cross effect if its button is pressed
applyFancyEffect(now, state.bigButton);
// Handle automatic triggering of ripples
processAutoTrigger(now);
// Create a drawing context with the current time and LED array
Fx::DrawContext ctx(now, leds);
// Draw the blended result of both wave layers to the LED array
fxBlend.draw(ctx);
// Send the color data to the actual LEDs
FastLED.show();
}