401 lines
18 KiB
C++
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();
|
|
}
|