812 lines
29 KiB
C++
812 lines
29 KiB
C++
/*
|
|
Festival Stick - Corkscrew LED Mapping Demo
|
|
|
|
This example demonstrates proper corkscrew LED mapping for a festival stick
|
|
(19+ turns, 288 LEDs) using the new Corkscrew ScreenMap functionality.
|
|
|
|
Key Features:
|
|
- Uses Corkscrew.toScreenMap() for accurate web interface visualization
|
|
- Draws patterns into a rectangular grid (frameBuffer)
|
|
- Maps the rectangular grid to the corkscrew LED positions using readFrom()
|
|
- Supports both noise patterns and manual LED positioning
|
|
- Proper color boost and brightness controls
|
|
|
|
Workflow:
|
|
1. Draw patterns into frameBuffer (rectangular grid for easy 2D drawing)
|
|
2. Use corkscrew.readFrom(frameBuffer) to map grid to corkscrew LED positions
|
|
3. Display the corkscrew buffer directly via FastLED
|
|
4. Web interface shows actual corkscrew spiral shape via ScreenMap
|
|
|
|
*/
|
|
|
|
|
|
#include "FastLED.h"
|
|
#include "fl/compiler_control.h"
|
|
|
|
|
|
#include "fl/assert.h"
|
|
#include "fl/corkscrew.h"
|
|
#include "fl/grid.h"
|
|
#include "fl/leds.h"
|
|
#include "fl/screenmap.h"
|
|
#include "fl/sstream.h"
|
|
#include "fl/warn.h"
|
|
#include "noise.h"
|
|
#include "fl/array.h"
|
|
#include "fx/2d/wave.h"
|
|
#include "fx/2d/blend.h"
|
|
#include "fx/fx_engine.h"
|
|
#include "fx/2d/animartrix.hpp"
|
|
|
|
// #include "vec3.h"
|
|
|
|
using namespace fl;
|
|
|
|
|
|
|
|
#ifndef PIN_DATA
|
|
#define PIN_DATA 1 // Universally available pin
|
|
#endif
|
|
|
|
#ifndef PIN_CLOCK
|
|
#define PIN_CLOCK 2 // Universally available pin
|
|
#endif
|
|
|
|
|
|
#ifdef TEST
|
|
#define NUM_LEDS 4
|
|
#define CORKSCREW_TURNS 2 // Default to 19 turns
|
|
#else
|
|
#define NUM_LEDS 288
|
|
#define CORKSCREW_TURNS 19.25 // Default to 19 turns
|
|
#endif
|
|
|
|
// #define CM_BETWEEN_LEDS 1.0 // 1cm between LEDs
|
|
// #define CM_LED_DIAMETER 0.5 // 0.5cm LED diameter
|
|
|
|
UITitle festivalStickTitle("Festival Stick - Advanced Version");
|
|
UIDescription festivalStickDescription(
|
|
"# Festival Stick Demo\n\n"
|
|
"This example demonstrates **proper corkscrew LED mapping** for a festival stick using FastLED's advanced mapping capabilities.\n\n"
|
|
"## Key Features\n"
|
|
"- **19+ turns** with 288 LEDs total\n"
|
|
"- Uses `Corkscrew.toScreenMap()` for accurate web interface visualization\n"
|
|
"- Multiple render modes: **Noise**, **Position**, **Fire**, **Wave**, and **Animartrix** effects\n"
|
|
"- Real-time cylindrical surface mapping\n"
|
|
"- **Wave mode**: Cylindrical 2D wave simulation with ripple effects and configurable blur\n"
|
|
"- **Animartrix mode**: Advanced 2D animation effects with polar coordinate patterns\n\n"
|
|
"## How It Works\n"
|
|
"1. Draws patterns into a rectangular grid (`frameBuffer`)\n"
|
|
"2. Maps the grid to corkscrew LED positions using `readFrom()`\n"
|
|
"3. Web interface shows the actual spiral shape via ScreenMap\n\n"
|
|
"*Select different render modes and adjust parameters to see various effects!*");
|
|
|
|
// UIHelp festivalStickHelp("Festival Stick - Advanced Guide");
|
|
|
|
// UIHelp corkscrewMappingHelp("Understanding Corkscrew Mapping");
|
|
|
|
// UIHelp uiControlsHelp("UI Controls Guide");
|
|
|
|
|
|
|
|
UISlider speed("Speed", 0.1f, 0.01f, 1.0f, 0.01f);
|
|
UISlider positionCoarse("Position Coarse (10x)", 0.0f, 0.0f, 1.0f, 0.01f);
|
|
UISlider positionFine("Position Fine (1x)", 0.0f, 0.0f, 0.1f, 0.001f);
|
|
UISlider positionExtraFine("Position Extra Fine (0.1x)", 0.0f, 0.0f, 0.01f, 0.0001f);
|
|
UISlider brightness("Brightness", 255, 0, 255, 1);
|
|
|
|
UICheckbox autoAdvance("Auto Advance", true);
|
|
UICheckbox allWhite("All White", false);
|
|
UICheckbox splatRendering("Splat Rendering", true);
|
|
|
|
// Noise controls (grouped under noiseGroup)
|
|
|
|
UISlider noiseScale("Noise Scale", 100, 10, 200, 5);
|
|
UISlider noiseSpeed("Noise Speed", 4, 1, 100, 1);
|
|
|
|
// UIDropdown examples - noise-related color palette
|
|
string paletteOptions[] = {"Party", "Heat", "Ocean", "Forest", "Rainbow"};
|
|
string renderModeOptions[] = { "Wave", "Animartrix", "Noise", "Position", "Fire" };
|
|
|
|
|
|
UIDropdown paletteDropdown("Color Palette", paletteOptions);
|
|
UIDropdown renderModeDropdown("Render Mode", renderModeOptions);
|
|
|
|
|
|
// fl::array<fl::pair<int, fl::string>> easeInfo = {
|
|
// pair(EASE_IN_QUAD, "EASE_IN_QUAD"),
|
|
// pair(EASE_OUT_QUAD, "EASE_OUT_QUAD"),
|
|
// pair(EASE_IN_OUT_QUAD, "EASE_IN_OUT_QUAD"),
|
|
// pair(EASE_IN_CUBIC, "EASE_IN_CUBIC"),
|
|
// pair(EASE_OUT_CUBIC, "EASE_OUT_CUBIC"),
|
|
// pair(EASE_IN_OUT_CUBIC, "EASE_IN_OUT_CUBIC"),
|
|
// pair(EASE_IN_SINE, "EASE_IN_SINE"),
|
|
// pair(EASE_OUT_SINE, "EASE_OUT_SINE"),
|
|
// pair(EASE_IN_OUT_SINE, "EASE_IN_OUT_SINE")
|
|
// };
|
|
|
|
|
|
fl::vector<fl::string> easeInfo = {
|
|
"EASE_NONE",
|
|
"EASE_IN_QUAD",
|
|
"EASE_OUT_QUAD",
|
|
"EASE_IN_OUT_QUAD",
|
|
"EASE_IN_CUBIC",
|
|
"EASE_OUT_CUBIC",
|
|
"EASE_IN_OUT_CUBIC",
|
|
"EASE_IN_SINE",
|
|
"EASE_OUT_SINE",
|
|
"EASE_IN_OUT_SINE"
|
|
};
|
|
|
|
EaseType getEaseType(fl::string value) {
|
|
if (value == "EASE_NONE") {
|
|
return EASE_NONE;
|
|
} else if (value == "EASE_IN_QUAD") {
|
|
return EASE_IN_QUAD;
|
|
} else if (value == "EASE_OUT_QUAD") {
|
|
return EASE_OUT_QUAD;
|
|
} else if (value == "EASE_IN_OUT_QUAD") {
|
|
return EASE_IN_OUT_QUAD;
|
|
} else if (value == "EASE_IN_CUBIC") {
|
|
return EASE_IN_CUBIC;
|
|
} else if (value == "EASE_OUT_CUBIC") {
|
|
return EASE_OUT_CUBIC;
|
|
} else if (value == "EASE_IN_OUT_CUBIC") {
|
|
return EASE_IN_OUT_CUBIC;
|
|
} else if (value == "EASE_IN_SINE") {
|
|
return EASE_IN_SINE;
|
|
} else if (value == "EASE_OUT_SINE") {
|
|
return EASE_OUT_SINE;
|
|
} else if (value == "EASE_IN_OUT_SINE") {
|
|
return EASE_IN_OUT_SINE;
|
|
} else {
|
|
return EASE_NONE;
|
|
}
|
|
}
|
|
|
|
// Color boost controls
|
|
UIDropdown saturationFunction("Saturation Function", easeInfo);
|
|
UIDropdown luminanceFunction("Luminance Function", easeInfo);
|
|
|
|
// Fire-related UI controls (added for cylindrical fire effect)
|
|
UISlider fireScaleXY("Fire Scale", 8, 1, 100, 1);
|
|
UISlider fireSpeedY("Fire SpeedY", 1.3, 1, 6, .1);
|
|
UISlider fireScaleX("Fire ScaleX", .3, 0.1, 3, .01);
|
|
UISlider fireInvSpeedZ("Fire Inverse SpeedZ", 20, 1, 100, 1);
|
|
UINumberField firePalette("Fire Palette", 0, 0, 2);
|
|
|
|
// Wave-related UI controls (cylindrical wave effects)
|
|
UISlider waveSpeed("Wave Speed", 0.03f, 0.0f, 1.0f, 0.01f);
|
|
UISlider waveDampening("Wave Dampening", 9.1f, 0.0f, 20.0f, 0.1f);
|
|
UICheckbox waveHalfDuplex("Wave Half Duplex", true);
|
|
UICheckbox waveAutoTrigger("Wave Auto Trigger", true);
|
|
UISlider waveTriggerSpeed("Wave Trigger Speed", 0.5f, 0.0f, 1.0f, 0.01f);
|
|
UIButton waveTriggerButton("Trigger Wave");
|
|
UINumberField wavePalette("Wave Palette", 0, 0, 2);
|
|
|
|
// Wave blur controls (added for smoother wave effects)
|
|
UISlider waveBlurAmount("Wave Blur Amount", 50, 0, 172, 1);
|
|
UISlider waveBlurPasses("Wave Blur Passes", 1, 1, 10, 1);
|
|
|
|
// Fire color palettes (from FireCylinder)
|
|
DEFINE_GRADIENT_PALETTE(firepal){
|
|
0, 0, 0, 0,
|
|
32, 255, 0, 0,
|
|
190, 255, 255, 0,
|
|
255, 255, 255, 255
|
|
};
|
|
|
|
DEFINE_GRADIENT_PALETTE(electricGreenFirePal){
|
|
0, 0, 0, 0,
|
|
32, 0, 70, 0,
|
|
190, 57, 255, 20,
|
|
255, 255, 255, 255
|
|
};
|
|
|
|
DEFINE_GRADIENT_PALETTE(electricBlueFirePal){
|
|
0, 0, 0, 0,
|
|
32, 0, 0, 70,
|
|
128, 20, 57, 255,
|
|
255, 255, 255, 255
|
|
};
|
|
|
|
// Wave color palettes (for cylindrical wave effects)
|
|
DEFINE_GRADIENT_PALETTE(waveBluepal){
|
|
0, 0, 0, 0, // Black (no wave)
|
|
32, 0, 0, 70, // Dark blue (low wave)
|
|
128, 20, 57, 255, // Electric blue (medium wave)
|
|
255, 255, 255, 255 // White (high wave)
|
|
};
|
|
|
|
DEFINE_GRADIENT_PALETTE(waveGreenpal){
|
|
0, 0, 0, 0, // Black (no wave)
|
|
8, 128, 64, 64, // Green with red tint (very low wave)
|
|
16, 255, 222, 222, // Pinkish red (low wave)
|
|
64, 255, 255, 255, // White (medium wave)
|
|
255, 255, 255, 255 // White (high wave)
|
|
};
|
|
|
|
DEFINE_GRADIENT_PALETTE(waveRainbowpal){
|
|
0, 255, 0, 0, // Red (no wave)
|
|
64, 255, 127, 0, // Orange (low wave)
|
|
128, 255, 255, 0, // Yellow (medium wave)
|
|
192, 0, 255, 0, // Green (high wave)
|
|
255, 0, 0, 255 // Blue (maximum wave)
|
|
};
|
|
|
|
// Create UIGroup for noise controls using variadic constructor
|
|
// This automatically assigns all specified controls to the "Noise Controls" group
|
|
UIGroup noiseGroup("Noise Controls", noiseScale, noiseSpeed, paletteDropdown);
|
|
UIGroup fireGroup("Fire Controls", fireScaleXY, fireSpeedY, fireScaleX, fireInvSpeedZ, firePalette);
|
|
UIGroup waveGroup("Wave Controls", waveSpeed, waveDampening, waveHalfDuplex, waveAutoTrigger, waveTriggerSpeed, waveTriggerButton, wavePalette, waveBlurAmount, waveBlurPasses);
|
|
UIGroup renderGroup("Render Options", renderModeDropdown, splatRendering, allWhite, brightness);
|
|
UIGroup colorBoostGroup("Color Boost", saturationFunction, luminanceFunction);
|
|
UIGroup pointGraphicsGroup("Point Graphics Mode", speed, positionCoarse, positionFine, positionExtraFine, autoAdvance);
|
|
|
|
// Animartrix-related UI controls
|
|
UINumberField animartrixIndex("Animartrix Animation", 5, 0, NUM_ANIMATIONS - 1);
|
|
UINumberField animartrixColorOrder("Animartrix Color Order", 0, 0, 5);
|
|
UISlider animartrixTimeSpeed("Animartrix Time Speed", 1, -10, 10, .1);
|
|
|
|
UIGroup animartrixGroup("Animartrix Controls", animartrixIndex, animartrixTimeSpeed, animartrixColorOrder);
|
|
|
|
// Color palette for noise
|
|
CRGBPalette16 noisePalette = PartyColors_p;
|
|
uint8_t colorLoop = 1;
|
|
|
|
// Option 1: Runtime Corkscrew (flexible, configurable at runtime)
|
|
Corkscrew corkscrew(CORKSCREW_TURNS, NUM_LEDS);
|
|
|
|
// Simple position tracking - one variable for both modes
|
|
static float currentPosition = 0.0f;
|
|
static uint32_t lastUpdateTime = 0;
|
|
|
|
// Wave effect globals
|
|
static uint32_t nextWaveTrigger = 0;
|
|
|
|
|
|
|
|
|
|
// Option 2: Constexpr dimensions for compile-time array sizing
|
|
constexpr uint16_t CORKSCREW_WIDTH =
|
|
calculateCorkscrewWidth(CORKSCREW_TURNS, NUM_LEDS);
|
|
constexpr uint16_t CORKSCREW_HEIGHT =
|
|
calculateCorkscrewHeight(CORKSCREW_TURNS, NUM_LEDS);
|
|
|
|
// Now you can use these for array initialization:
|
|
// CRGB frameBuffer[CORKSCREW_WIDTH * CORKSCREW_HEIGHT]; // Compile-time sized
|
|
// array
|
|
|
|
// Create a corkscrew with:
|
|
// - 30cm total length (300mm)
|
|
// - 5cm width (50mm)
|
|
// - 2mm LED inner diameter
|
|
// - 24 LEDs per turn
|
|
// ScreenMap screenMap = makeCorkScrew(NUM_LEDS,
|
|
// 300.0f, 50.0f, 2.0f, 24.0f);
|
|
|
|
// vector<vec3f> mapCorkScrew = makeCorkScrew(args);
|
|
ScreenMap screenMap;
|
|
fl::shared_ptr<Grid<CRGB>> frameBufferPtr;
|
|
|
|
// Wave effect objects - declared here but initialized in setup()
|
|
WaveFxPtr waveFx;
|
|
Blend2dPtr waveBlend;
|
|
|
|
// Animartrix effect objects - declared here but initialized in setup()
|
|
fl::unique_ptr<Animartrix> animartrix;
|
|
fl::unique_ptr<FxEngine> fxEngine;
|
|
WaveCrgbGradientMapPtr crgMap = fl::make_shared<WaveCrgbGradientMap>();
|
|
|
|
void setup() {
|
|
// Use constexpr dimensions (computed at compile time)
|
|
constexpr int width = CORKSCREW_WIDTH; // = 16
|
|
constexpr int height = CORKSCREW_HEIGHT; // = 18
|
|
|
|
|
|
// Noise controls are now automatically grouped by the UIGroup constructor
|
|
// The noiseGroup variadic constructor automatically called setGroup() on all controls
|
|
|
|
|
|
// Or use runtime corkscrew for dynamic sizing
|
|
// int width = corkscrew.cylinder_width();
|
|
// int height = corkscrew.cylinder_height();
|
|
|
|
XYMap xyMap = XYMap::constructRectangularGrid(width, height, 0);
|
|
|
|
// Use the corkscrew's internal buffer for the LED strip
|
|
CLEDController *controller =
|
|
&FastLED.addLeds<APA102HD, PIN_DATA, PIN_CLOCK, BGR>(corkscrew.rawData(), NUM_LEDS);
|
|
|
|
// CLEDController *controller =
|
|
// &FastLED.addLeds<WS2812, 3, BGR>(stripLeds, NUM_LEDS);
|
|
|
|
// NEW: Create ScreenMap directly from Corkscrew using toScreenMap()
|
|
// This maps each LED index to its exact position on the corkscrew spiral
|
|
// instead of using a rectangular grid mapping
|
|
ScreenMap corkscrewScreenMap = corkscrew.toScreenMap(0.2f);
|
|
|
|
// OLD WAY (rectangular grid - not accurate for corkscrew visualization):
|
|
// ScreenMap screenMap = xyMap.toScreenMap();
|
|
// screenMap.setDiameter(.2f);
|
|
|
|
// Set the corkscrew screen map for the controller
|
|
// This allows the web interface to display the actual corkscrew spiral shape
|
|
controller->setScreenMap(corkscrewScreenMap);
|
|
|
|
// Initialize wave effects for cylindrical surface
|
|
XYMap xyRect(width, height, false); // Rectangular grid for wave simulation
|
|
WaveFx::Args waveArgs;
|
|
waveArgs.factor = SuperSample::SUPER_SAMPLE_2X; // 2x supersampling for smoother waves
|
|
waveArgs.half_duplex = true; // Only positive waves
|
|
waveArgs.auto_updates = true; // Auto-update simulation
|
|
waveArgs.speed = 0.16f; // Wave propagation speed
|
|
waveArgs.dampening = 6.0f; // Wave energy loss
|
|
waveArgs.x_cyclical = true; // Enable cylindrical wrapping!
|
|
waveArgs.crgbMap = fl::make_shared<WaveCrgbGradientMap>(waveBluepal); // Default color palette
|
|
|
|
// Create wave effect with cylindrical mapping
|
|
waveFx = fl::make_shared<WaveFx>(xyRect, waveArgs);
|
|
|
|
// Create blender for wave effects (allows multiple wave layers in future)
|
|
waveBlend = fl::make_shared<Blend2d>(xyRect);
|
|
waveBlend->add(waveFx);
|
|
|
|
// Initialize Animartrix effect
|
|
XYMap animartrixXyMap = XYMap::constructRectangularGrid(width, height, 0);
|
|
animartrix.reset(new Animartrix(animartrixXyMap, POLAR_WAVES));
|
|
fxEngine.reset(new FxEngine(width * height));
|
|
fxEngine->addFx(*animartrix);
|
|
|
|
// Demonstrate UIGroup functionality for noise controls
|
|
FL_WARN("Noise UI Group initialized: " << noiseGroup.name());
|
|
FL_WARN(" This group contains noise pattern controls:");
|
|
FL_WARN(" - Use Noise Pattern toggle");
|
|
FL_WARN(" - Noise Scale and Speed sliders");
|
|
FL_WARN(" - Color Palette selection for noise");
|
|
FL_WARN(" UIGroup automatically applied group membership via variadic constructor");
|
|
|
|
// Set initial dropdown selections
|
|
paletteDropdown.setSelectedIndex(0); // Party
|
|
renderModeDropdown.setSelectedIndex(0); // Fire (new default)
|
|
|
|
// Add onChange callbacks for dropdowns
|
|
paletteDropdown.onChanged([](UIDropdown &dropdown) {
|
|
string selectedPalette = dropdown.value();
|
|
FL_WARN("Noise palette changed to: " << selectedPalette);
|
|
if (selectedPalette == "Party") {
|
|
noisePalette = PartyColors_p;
|
|
} else if (selectedPalette == "Heat") {
|
|
noisePalette = HeatColors_p;
|
|
} else if (selectedPalette == "Ocean") {
|
|
noisePalette = OceanColors_p;
|
|
} else if (selectedPalette == "Forest") {
|
|
noisePalette = ForestColors_p;
|
|
} else if (selectedPalette == "Rainbow") {
|
|
noisePalette = RainbowColors_p;
|
|
}
|
|
});
|
|
|
|
renderModeDropdown.onChanged([](UIDropdown &dropdown) {
|
|
string mode = dropdown.value();
|
|
// Simple example of using getOption()
|
|
for(size_t i = 0; i < dropdown.getOptionCount(); i++) {
|
|
if(dropdown.getOption(i) == mode) {
|
|
FL_WARN("Render mode changed to: " << mode);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add onChange callback for animartrix color order
|
|
animartrixColorOrder.onChanged([](int value) {
|
|
EOrder order = RGB;
|
|
switch(value) {
|
|
case 0: order = RGB; break;
|
|
case 1: order = RBG; break;
|
|
case 2: order = GRB; break;
|
|
case 3: order = GBR; break;
|
|
case 4: order = BRG; break;
|
|
case 5: order = BGR; break;
|
|
}
|
|
if (animartrix.get()) {
|
|
animartrix->setColorOrder(order);
|
|
}
|
|
});
|
|
|
|
|
|
waveFx->setCrgbMap(crgMap);
|
|
|
|
frameBufferPtr = corkscrew.getOrCreateInputSurface();
|
|
}
|
|
|
|
|
|
FL_OPTIMIZATION_LEVEL_O0_BEGIN // Works around a compile bug in clang 19
|
|
float get_position(uint32_t now) {
|
|
if (autoAdvance.value()) {
|
|
// Check if auto-advance was just enabled
|
|
// Auto-advance mode: increment smoothly from current position
|
|
float elapsedSeconds = float(now - lastUpdateTime) / 1000.0f;
|
|
float increment = elapsedSeconds * speed.value() *
|
|
0.3f; // Make it 1/20th the original speed
|
|
currentPosition = fmodf(currentPosition + increment, 1.0f);
|
|
lastUpdateTime = now;
|
|
return currentPosition;
|
|
} else {
|
|
// Manual mode: use the dual slider control
|
|
float combinedPosition = positionCoarse.value() + positionFine.value() + positionExtraFine.value();
|
|
// Clamp to ensure we don't exceed 1.0
|
|
if (combinedPosition > 1.0f)
|
|
combinedPosition = 1.0f;
|
|
return combinedPosition;
|
|
}
|
|
}
|
|
FL_OPTIMIZATION_LEVEL_O0_END
|
|
|
|
void fillFrameBufferNoise() {
|
|
// Get current UI values
|
|
uint8_t noise_scale = noiseScale.value();
|
|
uint8_t noise_speed = noiseSpeed.value();
|
|
|
|
// Derive noise coordinates from current time instead of forward iteration
|
|
uint32_t now = millis();
|
|
uint16_t noise_z = now * noise_speed / 10; // Primary time dimension
|
|
uint16_t noise_x = now * noise_speed / 80; // Slow drift in x
|
|
uint16_t noise_y = now * noise_speed / 160; // Even slower drift in y (opposite direction)
|
|
|
|
int width = frameBufferPtr->width();
|
|
int height = frameBufferPtr->height();
|
|
|
|
// Data smoothing for low speeds (from NoisePlusPalette example)
|
|
uint8_t dataSmoothing = 0;
|
|
if(noise_speed < 50) {
|
|
dataSmoothing = 200 - (noise_speed * 4);
|
|
}
|
|
|
|
// Generate noise for each pixel in the frame buffer using cylindrical mapping
|
|
for(int x = 0; x < width; x++) {
|
|
for(int y = 0; y < height; y++) {
|
|
// Convert rectangular coordinates to cylindrical coordinates
|
|
// Map x to angle (0 to 2*PI), y remains as height
|
|
float angle = (float(x) / float(width)) * 2.0f * PI;
|
|
|
|
// Convert cylindrical coordinates to cartesian for noise sampling
|
|
// Use the noise_scale to control the cylinder size in noise space
|
|
float cylinder_radius = noise_scale; // Use the existing noise_scale parameter
|
|
|
|
// Calculate cartesian coordinates on the cylinder surface
|
|
float noise_x_cyl = cos(angle) * cylinder_radius;
|
|
float noise_y_cyl = sin(angle) * cylinder_radius;
|
|
float noise_z_height = float(y) * noise_scale; // Height component
|
|
|
|
// Apply time-based offsets
|
|
int xoffset = int(noise_x_cyl) + noise_x;
|
|
int yoffset = int(noise_y_cyl) + noise_y;
|
|
int zoffset = int(noise_z_height) + noise_z;
|
|
|
|
// Generate 8-bit noise value using 3D Perlin noise with cylindrical coordinates
|
|
uint8_t data = inoise8(xoffset, yoffset, zoffset);
|
|
|
|
// Expand the range from ~16-238 to 0-255 (from NoisePlusPalette)
|
|
data = qsub8(data, 16);
|
|
data = qadd8(data, scale8(data, 39));
|
|
|
|
// Apply data smoothing if enabled
|
|
if(dataSmoothing) {
|
|
CRGB oldColor = frameBufferPtr->at(x, y);
|
|
uint8_t olddata = (oldColor.r + oldColor.g + oldColor.b) / 3; // Simple brightness extraction
|
|
uint8_t newdata = scale8(olddata, dataSmoothing) + scale8(data, 256 - dataSmoothing);
|
|
data = newdata;
|
|
}
|
|
|
|
// Map noise to color using palette (adapted from NoisePlusPalette)
|
|
uint8_t index = data;
|
|
uint8_t bri = data;
|
|
|
|
// Add color cycling if enabled - also derive from time
|
|
uint8_t ihue = 0;
|
|
if(colorLoop) {
|
|
ihue = (now / 100) % 256; // Derive hue from time instead of incrementing
|
|
index += ihue;
|
|
}
|
|
|
|
// Enhance brightness (from NoisePlusPalette example)
|
|
// if(bri > 127) {
|
|
// //bri = 255;
|
|
// } else {
|
|
// //bri = dim8_raw(bri * 2);
|
|
// }
|
|
|
|
// Get color from palette and set pixel
|
|
CRGB color = ColorFromPalette(noisePalette, index, bri);
|
|
|
|
// Apply color boost using ease functions
|
|
EaseType sat_ease = getEaseType(saturationFunction.value());
|
|
EaseType lum_ease = getEaseType(luminanceFunction.value());
|
|
color = color.colorBoost(sat_ease, lum_ease);
|
|
|
|
frameBufferPtr->at(x, y) = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
void drawNoise(uint32_t now) {
|
|
FL_UNUSED(now);
|
|
fillFrameBufferNoise();
|
|
}
|
|
|
|
void draw(float pos) {
|
|
if (splatRendering) {
|
|
Tile2x2_u8_wrap pos_tile = corkscrew.at_wrap(pos);
|
|
//FL_WARN("pos_tile: " << pos_tile);
|
|
CRGB color = CRGB::Blue;
|
|
// Apply color boost using ease functions
|
|
EaseType sat_ease = getEaseType(saturationFunction.value());
|
|
EaseType lum_ease = getEaseType(luminanceFunction.value());
|
|
color = color.colorBoost(sat_ease, lum_ease);
|
|
// Draw each pixel in the 2x2 tile using the new wrapping API
|
|
for (int dx = 0; dx < 2; ++dx) {
|
|
for (int dy = 0; dy < 2; ++dy) {
|
|
Tile2x2_u8_wrap::Entry data = pos_tile.at(dx, dy);
|
|
vec2<u16> wrapped_pos = data.first; // Already wrapped position
|
|
uint8_t alpha = data.second; // Alpha value
|
|
|
|
if (alpha > 0) { // Only draw if there's some alpha
|
|
CRGB c = color;
|
|
c.nscale8(alpha); // Scale the color by the alpha value
|
|
frameBufferPtr->at(wrapped_pos.x, wrapped_pos.y) = c;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// None splat rendering, looks aweful.
|
|
vec2f pos_vec2f = corkscrew.at_no_wrap(pos);
|
|
vec2<u16> pos_i16 = vec2<u16>(pos_vec2f.x, pos_vec2f.y);
|
|
|
|
CRGB color = CRGB::Blue;
|
|
// Apply color boost using ease functions
|
|
EaseType sat_ease = getEaseType(saturationFunction.value());
|
|
EaseType lum_ease = getEaseType(luminanceFunction.value());
|
|
color = color.colorBoost(sat_ease, lum_ease);
|
|
|
|
// Now map the cork screw position to the cylindrical buffer that we
|
|
// will draw.
|
|
frameBufferPtr->at(pos_i16.x, pos_i16.y) = color; // Draw a blue pixel at (w, h)
|
|
}
|
|
}
|
|
|
|
CRGBPalette16 getFirePalette() {
|
|
int paletteIndex = (int)firePalette.value();
|
|
switch (paletteIndex) {
|
|
case 0:
|
|
return firepal;
|
|
case 1:
|
|
return electricGreenFirePal;
|
|
case 2:
|
|
return electricBlueFirePal;
|
|
default:
|
|
return firepal;
|
|
}
|
|
}
|
|
|
|
uint8_t getFirePaletteIndex(uint32_t millis32, int width, int max_width, int height, int max_height,
|
|
uint32_t y_speed) {
|
|
uint16_t scale = fireScaleXY.as<uint16_t>();
|
|
|
|
float xf = (float)width / (float)max_width;
|
|
uint8_t x = (uint8_t)(xf * 255);
|
|
|
|
uint32_t cosx = cos8(x);
|
|
uint32_t sinx = sin8(x);
|
|
|
|
float trig_scale = scale * fireScaleX.value();
|
|
cosx *= trig_scale;
|
|
sinx *= trig_scale;
|
|
|
|
uint32_t y = height * scale + y_speed;
|
|
|
|
uint16_t z = millis32 / fireInvSpeedZ.as<uint16_t>();
|
|
|
|
uint16_t noise16 = inoise16(cosx << 8, sinx << 8, y << 8, z << 8);
|
|
|
|
uint8_t noise_val = noise16 >> 8;
|
|
|
|
int8_t subtraction_factor = abs8(height - (max_height - 1)) * 255 /
|
|
(max_height - 1);
|
|
|
|
return qsub8(noise_val, subtraction_factor);
|
|
}
|
|
|
|
void fillFrameBufferFire(uint32_t now) {
|
|
CRGBPalette16 myPal = getFirePalette();
|
|
|
|
// Calculate the current y-offset for animation (makes the fire move)
|
|
uint32_t y_speed = now * fireSpeedY.value();
|
|
|
|
int width = frameBufferPtr->width();
|
|
int height = frameBufferPtr->height();
|
|
|
|
// Loop through every pixel in our cylindrical matrix
|
|
for (int w = 0; w < width; w++) {
|
|
for (int h = 0; h < height; h++) {
|
|
// Calculate which color to use from our palette for this pixel
|
|
uint8_t palette_index =
|
|
getFirePaletteIndex(now, w, width, h, height, y_speed);
|
|
|
|
// Get the actual RGB color from the palette
|
|
CRGB color = ColorFromPalette(myPal, palette_index, 255);
|
|
|
|
// Apply color boost using ease functions
|
|
EaseType sat_ease = getEaseType(saturationFunction.value());
|
|
EaseType lum_ease = getEaseType(luminanceFunction.value());
|
|
color = color.colorBoost(sat_ease, lum_ease);
|
|
|
|
// Set the pixel in the frame buffer
|
|
// Flip coordinates to make fire rise from bottom
|
|
frameBufferPtr->at((width - 1) - w, (height - 1) - h) = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
void drawFire(uint32_t now) {
|
|
fillFrameBufferFire(now);
|
|
}
|
|
|
|
// Wave effect helper functions
|
|
CRGBPalette16 getWavePalette() {
|
|
int paletteIndex = (int)wavePalette.value();
|
|
switch (paletteIndex) {
|
|
case 0:
|
|
return waveBluepal; // Electric blue waves
|
|
case 1:
|
|
return waveGreenpal; // Green/red waves
|
|
case 2:
|
|
return waveRainbowpal; // Rainbow waves
|
|
default:
|
|
return waveBluepal; // Default to blue
|
|
}
|
|
}
|
|
|
|
void triggerWaveRipple() {
|
|
// Create a ripple at a random position within the central area
|
|
float perc = 0.15f; // 15% margin from edges
|
|
int width = corkscrew.cylinderWidth();
|
|
int height = corkscrew.cylinderHeight();
|
|
|
|
int min_x = perc * width;
|
|
int max_x = (1 - perc) * width;
|
|
int min_y = perc * height;
|
|
int max_y = (1 - perc) * height;
|
|
|
|
int x = random8(min_x, max_x);
|
|
int y = random8(min_y, max_y);
|
|
|
|
// Trigger a 2x2 wave ripple for more punch (compensates for blur reduction)
|
|
float ripple_strength = 1.5f; // Higher value for more impact
|
|
waveFx->setf(x, y, ripple_strength);
|
|
waveFx->setf(x + 1, y, ripple_strength);
|
|
waveFx->setf(x, y + 1, ripple_strength);
|
|
waveFx->setf(x + 1, y + 1, ripple_strength);
|
|
|
|
FL_WARN("Wave ripple triggered at (" << x << ", " << y << ") with 2x2 pattern");
|
|
}
|
|
|
|
void processWaveAutoTrigger(uint32_t now) {
|
|
// Handle automatic wave triggering
|
|
if (waveAutoTrigger.value()) {
|
|
if (now >= nextWaveTrigger) {
|
|
triggerWaveRipple();
|
|
|
|
// Calculate next trigger time based on speed
|
|
float speed = 1.0f - waveTriggerSpeed.value();
|
|
uint32_t min_interval = (uint32_t)(500 * speed); // Minimum 500ms * speed
|
|
uint32_t max_interval = (uint32_t)(3000 * speed); // Maximum 3000ms * speed
|
|
|
|
// Ensure valid range
|
|
uint32_t min = MIN(min_interval, max_interval);
|
|
uint32_t max = MAX(min_interval, max_interval);
|
|
if (min >= max) max = min + 1;
|
|
|
|
nextWaveTrigger = now + random16(min, max);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void drawWave(uint32_t now) {
|
|
// Update wave parameters from UI
|
|
waveFx->setSpeed(waveSpeed.value());
|
|
waveFx->setDampening(waveDampening.value());
|
|
waveFx->setHalfDuplex(waveHalfDuplex.value());
|
|
waveFx->setXCylindrical(true); // Always keep cylindrical for corkscrew
|
|
|
|
// Update wave color palette
|
|
CRGBPalette16 currentPalette = getWavePalette();
|
|
crgMap->setGradient(currentPalette);
|
|
|
|
|
|
|
|
// Apply blur settings to the wave blend (for smoother wave effects)
|
|
waveBlend->setGlobalBlurAmount(waveBlurAmount.value());
|
|
waveBlend->setGlobalBlurPasses(waveBlurPasses.value());
|
|
|
|
// Check if manual trigger button was pressed
|
|
if (waveTriggerButton.value()) {
|
|
triggerWaveRipple();
|
|
}
|
|
|
|
// Handle auto-triggering
|
|
processWaveAutoTrigger(now);
|
|
|
|
// Draw the wave effect directly to the frame buffer
|
|
// Create a DrawContext for the wave renderer
|
|
Fx::DrawContext waveContext(now, frameBufferPtr->data());
|
|
waveBlend->draw(waveContext);
|
|
}
|
|
|
|
void drawAnimartrix(uint32_t now) {
|
|
// Update animartrix parameters from UI
|
|
fxEngine->setSpeed(animartrixTimeSpeed.value());
|
|
|
|
// Handle animation index changes
|
|
static int lastAnimartrixIndex = -1;
|
|
if (animartrixIndex.value() != lastAnimartrixIndex) {
|
|
lastAnimartrixIndex = animartrixIndex.value();
|
|
animartrix->fxSet(animartrixIndex.value());
|
|
}
|
|
|
|
// Draw the animartrix effect directly to the frame buffer
|
|
CRGB* dst = corkscrew.rawData();
|
|
fxEngine->draw(now, dst);
|
|
}
|
|
|
|
void loop() {
|
|
|
|
delay(4);
|
|
uint32_t now = millis();
|
|
frameBufferPtr->clear();
|
|
|
|
if (allWhite) {
|
|
CRGB whiteColor = CRGB(8, 8, 8);
|
|
for (u32 x = 0; x < frameBufferPtr->width(); x++) {
|
|
for (u32 y = 0; y < frameBufferPtr->height(); y++) {
|
|
frameBufferPtr->at(x, y) = whiteColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Update the corkscrew mapping with auto-advance or manual position control
|
|
float combinedPosition = get_position(now);
|
|
float pos = combinedPosition * (corkscrew.size() - 1);
|
|
|
|
|
|
if (renderModeDropdown.value() == "Noise") {
|
|
drawNoise(now);
|
|
} else if (renderModeDropdown.value() == "Fire") {
|
|
drawFire(now);
|
|
} else if (renderModeDropdown.value() == "Wave") {
|
|
|
|
drawWave(now);
|
|
} else if (renderModeDropdown.value() == "Animartrix") {
|
|
drawAnimartrix(now);
|
|
} else {
|
|
draw(pos);
|
|
}
|
|
|
|
|
|
// Use the new readFrom workflow:
|
|
// 1. Read directly from the frameBuffer Grid into the corkscrew's internal buffer
|
|
// use_multi_sampling = true will use multi-sampling to sample from the source grid,
|
|
// this will give a little bit better accuracy and the screenmap will be more accurate.
|
|
const bool use_multi_sampling = splatRendering;
|
|
// corkscrew.readFrom(frameBuffer, use_multi_sampling);
|
|
corkscrew.draw(use_multi_sampling);
|
|
|
|
// The corkscrew's buffer is now populated and FastLED will display it directly
|
|
|
|
FastLED.setBrightness(brightness.value());
|
|
FastLED.show();
|
|
}
|